Handoff Trip → Agentforce (agente SDR)¶
Status: implementado atrás da flag
ENABLE_AGENTFORCE_HANDOFF(defaultfalse). Contrato técnico da Agent API:docs/agentforce/DAS_Integracao_A2A_IFriend.docx(Everymind, v1.1 — 23/09/2026). Os anexos da Everymind (docs/agentforce/) ficam só no repositório — são excluídos do site de docs.
1. Objetivo¶
Hoje todo handoff humano do Trip termina no formulário de WhatsApp (gerar_formulario_suporte):
o cliente cai direto no time humano do Service Console, qualificado ou não.
Com esta integração, nos handoffs o Trip escala primeiro para o agente SDR no Agentforce, que qualifica a oportunidade e decide:
- abrir um fluxo de Vendas para o time comercial; ou
- informar ao cliente que não há o produto.
O time humano passa a receber só cases qualificados.
Escopo desta fase¶
| Canal | Primeiro atendimento | Nesta fase |
|---|---|---|
| Site (webchat/SSE) | Trip | ✅ Trip → Agentforce (este documento) |
| WhatsApp oficial (Digital Engagement) | Agentforce | Everymind: Agentforce → Trip para busca de produto, via o servidor A2A de entrada do Trip (contrato de contexto na seção 9) |
2. Visão geral¶
flowchart LR
C[Cliente no site] -->|mensagens| T[Trip<br/>root_agent + sub-agentes]
T -->|busca produtos| CAT[(Catálogo iFriend)]
T -->|M1 · M2 · M3<br/>escalar_agentforce| AF[Agentforce<br/>agente SDR]
AF -->|qualifica| D{Decisão}
D -->|oportunidade| V[Fluxo de Vendas<br/>time comercial]
D -->|sem produto| C
T -.->|fallback: API fora| W[Formulário WhatsApp<br/>Service Console]
Modo relay: o cliente continua no chat do site. Depois do handoff, o Trip repassa cada mensagem do cliente para o Agentforce e mostra as respostas dele. O LLM do Trip fica fora do caminho até o Agentforce encerrar a sessão.
3. Os momentos em que o Trip chama o Agentforce¶
Pré-condição comum: identificação da conta¶
O Agentforce precisa saber quem é o cliente e de que tipo é a conta para abrir o Case e dar sequência. A conta é criada no Salesforce (pelo fluxo do Case). O Trip só identifica e coleta os dados mínimos, e nunca cria conta na iFriend.
| Tipo | tipo_conta |
Dados obrigatórios |
|---|---|---|
| Viajante (B2C) | viajante |
nome, e-mail, telefone/WhatsApp |
| Agência / empresa (B2B) | agencia |
nome do contato, e-mail, telefone/WhatsApp, nome da empresa e CNPJ |
| Situação | Comportamento do Trip |
|---|---|
| Logado (JWT) | Nome e e-mail vêm do jwt_context. O tipo vem das roles: ROLE_AFFILIATED* ou partner_code é agência; senão, viajante. A conta conta como existente. O Trip pede o telefone e, se for agência, empresa + CNPJ (o JWT não traz esses dados). |
| Anônimo com conta | Pergunta "Você já tem conta na iFriend? Qual o e-mail?" → buscar_usuario(email) (somente leitura). Se encontrar, conta_existente=true e ifriend_id é enviado. |
| Anônimo sem conta | Pergunta se a viagem é pessoal ou para uma agência/empresa e coleta os dados mínimos do tipo em uma única pergunta. A conta segue como nova, e o Salesforce a cria. |
| Agência (qualquer caso) | Com o CNPJ, buscar_agencia(cnpj) (somente leitura) diz se a agência já existe na iFriend. |
Validações na tool escalar_agentforce:
- Recusa sem os dados obrigatórios do tipo (
status="dados_incompletos", com a listafaltando). - CNPJ validado: 14 dígitos e dígitos verificadores. Se for inválido, pede para o cliente conferir.
- O tipo vindo do JWT tem precedência sobre o que o LLM informar.
Tabela de momentos¶
| # | Momento | Onde nasce no Trip | Gatilho | handoff_reason |
Contexto enviado |
|---|---|---|---|---|---|
| M1 | Produto não encontrado | discovery_agent, custom_tour_agent |
A busca não retorna nada que atenda ao pedido → o Trip oferece "Quer que eu te conecte com um especialista iFriend para verificar essa opção?" → o cliente aceita | produto_nao_encontrado |
destino, datas, nº de pessoas, termo buscado, tipo de produto |
| M2 | Pedido explícito de humano | support_agent (rota "falar com atendente/consultor/humano/WhatsApp") |
O cliente pede atendimento humano | pedido_humano (ou orcamento se a conversa era de orçamento) |
tópico, produto/ID se houver, resumo |
| M3 | Tour personalizado / orçamento | custom_tour_agent (HANDOFF PARA HUMANO); quote_agent via root_agent → support_agent |
Cliente sem acesso ao PDF (tem_acesso=false) terminou de montar o pedido, ou pediu consultor |
tour_personalizado / orcamento |
briefing estruturado de gerar_briefing_tour + produtos selecionados (ID, nome, preço, moeda) |
Case "Fluxo de Ocorrência de Vendas" (JSON estruturado)¶
Em todo handoff (M1, M2 e M3) o Trip monta o Case com os campos que capturou na conversa e o envia
pronto no formato do Composite Upsert do Salesforce, com os API names reais do objeto Case.
Referências: docs/agentforce/Fluxo_de_ocorrencia_de_vendas.docx (campos do fluxo),
Case_Fields.csv / Case_Object_Describe.json (API names e picklists) e
Exemplo_API_Composite_Upsert_Object.md (formato).
- M1/M3 (
produto_nao_encontrado,tour_personalizado,orcamento): os campos obrigatórios (*) precisam estar preenchidos. Sem eles, a tool devolvedados_incompletos, e o Trip pergunta o que falta em no máximo 2 mensagens. - M2 (
pedido_humano): o Case vai parcial, com o que já se sabe. Só a identificação é obrigatória. - Os campos "preenchido automaticamente" do Salesforce (Account Owner, Região, Priority, Qualificação,
Total de Pax...) e os lookups (
Solicitante__c,Lead__c,AccountId) não são enviados.
Upsert: PATCH /services/data/v62.0/composite/sobjects/Case/External_ID__c, com o corpo em
Sales_Case_Payload. External_ID__c = {sessão do Trip}:{uuid8}, a mesma externalSessionKey
da sessão no Agentforce. Isso torna o upsert idempotente e rastreável até a conversa no Trip.
| Seção (docx) | Campo | API name | Tipo / valores | Obrig. | Origem |
|---|---|---|---|---|---|
| — | Record Type | RecordTypeId |
012U4000001VcQDIA0 (Fluxo de Vendas; env AGENTFORCE_CASE_RECORD_TYPE_ID) |
* | Tool |
| — | External ID | External_ID__c |
chave externa da sessão | * | Tool |
| Description | Subject | Subject |
ex.: Cotação — Paris — 3 pax — 11/2026 |
* | Tool |
| Description | Description | Description |
resumo da conversa | * | Tool |
| Entrada | Case Reason | Reason |
Cotação |
* | Tool |
| Entrada | Case Origin | Origin |
Chat |
Tool | |
| Entrada | Tipo de Registro de Conta | Tipo_registro_conta__c |
B2C (viajante) / B2B (agência) |
Tool | |
| Entrada | CNPJ | CNPJ__c |
14 dígitos | * B2B | Identificação |
| Entrada | Contact Name | Nome__c |
nome do contato | Identificação | |
| Entrada | ID da Reserva | ID_da_Reserva_mais_de_uma__c |
texto | Trip | |
| Conta não identificada | Web Name / Email / Phone / Company | SuppliedName, SuppliedEmail, SuppliedPhone, SuppliedCompany |
só quando a conta é nova | Identificação | |
| Qualificação | Destino | Destino__c |
texto | * | Trip |
| Qualificação | Destinos adicionais | Destinos_adicionais__c |
texto (separado por vírgula) | Trip | |
| Qualificação | Data início da viagem | Data_da_Viagem__c |
date | * | Trip |
| Qualificação | Serviços solicitados | Servicos_solicitados__c |
multipicklist: Guia, Guia com carro, Ingresso_ticket, Experiência Regular, Experiencia_privativa, Transfer, Veículo à disposição, Outros |
Trip (mapeado) | |
| Qualificação | Já comprou as passagens? | Ja_comprou_as_passagens__c |
Sim / Não / Não respondeu |
* | Trip |
| Qualificação | Já reservou a hospedagem? | J_comprou_a_hospedagem__c |
Sim / Não / Não respondeu |
* | Trip |
| Qualificação | Valor/Orçamento predefinido | Valor__c |
currency (texto sem número vai para Informações adicionais) | Trip | |
| Mais informações | Quantidade de adultos / crianças | Quantidade_de_adultos__c, Quantidade_de_crian_as__c |
número | Trip | |
| Mais informações | Já possui roteiro | Ja_possui_roteiro__c |
Sim / Não |
Trip | |
| Mais informações | Serviços privativos ou em grupo | Tem_prefer_mcia__c |
Privativo / Grupo |
Trip | |
| Mais informações | Possui necessidade especial / Qual | Possui_alguma_necessidade_especial__c, Necessidades_especiais__c |
Sim/Não + texto (só se Sim) |
Trip | |
| Mais informações | Produto de interesse | Produto__c |
texto | Trip | |
| Exclusivo Transfer | Origem/Destino/Data/Hora Ida e Volta, malas | Origem_Ida__c, Destino_Ida__c, Data_Ida_viagem__c, Hora_Ida_da_Viagem__c, Origem_volta__c, Destino_Volta__c, Data_Volta_da_Viagem__c, Hora_Volta_viagem__c, Quantidade_de_malas_pequenas__c, Quantidade_de_malas_grandes__c |
só quando o interesse é transfer | Trip | |
| — | Informações adicionais | Informa_es_adicionais__c |
motivo do handoff, sessão/canal, conta nova/existente e IDs iFriend, produtos do catálogo considerados | Tool |
O LLM do Trip preenche um JSON de captura (chaves em PT: destino, data_inicio_viagem,
ja_comprou_passagens...; ver a docstring de escalar_agentforce). A tool valida, completa e converte
para os API names acima (integrations/agentforce/sales_case.py). Um teste confere cada campo e cada
valor de picklist contra o Case_Object_Describe.json.
Exemplo de Sales_Case_Payload (agência nova, produto não encontrado):
{
"allOrNone": true,
"records": [
{
"attributes": {"type": "Case"},
"RecordTypeId": "012U4000001VcQDIA0",
"External_ID__c": "sse_u1_ab12cd34:9f3c1a2b",
"Subject": "Cotação — Capadócia — 2 pax — 05/2026",
"Description": "Procura passeio de balão na Capadócia para 2 pessoas em maio",
"Reason": "Cotação",
"Origin": "Chat",
"Tipo_registro_conta__c": "B2B",
"CNPJ__c": "11222333000181",
"Nome__c": "Ana Souza",
"SuppliedName": "Ana Souza",
"SuppliedEmail": "ana@viajebem.com.br",
"SuppliedPhone": "5511999990000",
"SuppliedCompany": "Agência Viaje Bem",
"Destino__c": "Capadócia",
"Data_da_Viagem__c": "2026-05-10",
"Servicos_solicitados__c": "Experiência Regular",
"Ja_comprou_as_passagens__c": "Sim",
"J_comprou_a_hospedagem__c": "Não",
"Quantidade_de_adultos__c": 2,
"Quantidade_de_crian_as__c": 0,
"Produto__c": "passeio de balão",
"Informa_es_adicionais__c": "Origem: Trip (assistente do site) — motivo do handoff: produto_nao_encontrado\nSessão Trip: sse_u1_ab12cd34 | canal: sse\nConta: agencia — nova"
}
]
}
Transporte: o corpo acima vai inteiro em uma única variável de contexto (Sales_Case_Payload, nome
configurável em AGENTFORCE_CASE_VARIABLE). A 1ª mensagem de texto leva um resumo legível (seção 5).
Se decidir abrir o Fluxo de Vendas, o Agentforce faz o upsert com esse corpo, sem mapear campo a campo.
Regra de acesso: affiliate/admin não faz handoff de orçamento¶
Quem tem resolve_proposal_access(jwt_context) == True (ROLE_AFFILIATED_PARENT,
ROLE_AFFILIATED_EMPLOYEE, ROLE_*_ADMIN) gera a proposta sozinho pelo proposal_agent/gerar_proposta.
- M3 nunca dispara para esse usuário. Os prompts só escalam com
tem_acesso=false, e a própria tool devolvestatus="nao_permitido"paraorcamento/tour_personalizado(defesa em profundidade, no mesmo padrão do gate degerar_proposta). - M1 e M2 continuam disponíveis para qualquer usuário: produto inexistente e pedido explícito de humano.
Quando o Trip NÃO chama o Agentforce (usa o formulário de WhatsApp como antes)¶
- Sites whitelabel. O atendimento humano é o WhatsApp do affiliate, não o SDR iFriend
(
AGENTFORCE_ALLOW_WHITELABEL=falsepor padrão). - Canal fora de
AGENTFORCE_CHANNELS(padrãosse,webchat). - Agentforce indisponível ou já com falha nesta conversa (ver seção 6).
- Flag desligada ou credenciais ausentes.
4. Sequência por momento¶
M1 — Produto não encontrado (site)¶
sequenceDiagram
autonumber
actor C as Cliente
participant D as discovery_agent
participant T as escalar_agentforce
participant R as root_agent (relay)
participant AF as Agentforce (SDR)
C->>D: "Passeio de balão na Capadócia em maio, 2 pessoas"
D->>D: busca_produtos → []
D-->>C: Não encontrei online. Quer que eu te conecte com um especialista?
C->>D: Sim
D-->>C: Você já tem conta na iFriend? Qual o e-mail?
C->>D: Não tenho
D-->>C: A viagem é pessoal ou para uma agência/empresa?
C->>D: Agência — Ana, ana@viajebem.com.br, 11 99999-0000, Viaje Bem, CNPJ 11.222.333/0001-81
D->>D: buscar_agencia(cnpj) → não encontrada (conta nova)
D-->>C: Qual a data de início e quantos adultos/crianças?
C->>D: 10/05, 2 adultos
D-->>C: Já comprou as passagens? E a hospedagem?
C->>D: Passagens sim, hotel ainda não
D->>T: escalar_agentforce(produto_nao_encontrado, conta, case_vendas_json)
T->>T: valida conta (CNPJ) e campos * do Case
T->>AF: POST /agents/{id}/sessions (variables: Sales_Case_Payload)
T->>AF: POST /sessions/{sid}/messages (seq 1: resumo do contexto)
AF-->>T: resposta
T->>R: transfer_to_agent(root_agent)
R-->>C: resposta do Agentforce
loop até SessionEnded / inatividade
C->>R: mensagem
R->>AF: POST /messages (seq n)
AF-->>R: resposta
R-->>C: resposta
end
AF-->>R: SessionEnded (oportunidade aberta ou sem produto)
R-->>C: última mensagem — o Trip volta a atender
M2 — Pedido explícito de humano¶
Igual ao M1, mas começa no support_agent: o root_agent roteia para ele → identificação da conta →
escalar_agentforce(pedido_humano) com o Case parcial (o que já se souber da conversa, sem interrogatório).
M3 — Tour personalizado / orçamento sem acesso ao PDF¶
custom_tour_agent: ETAPA 1 (verificar_acesso_orcamento → tem_acesso=false) → ETAPA 2 (produtos
reais selecionados nos cards) → nº de participantes → gerar_briefing_tour → identificação da conta →
campos * do Case que faltarem (data de início, passagens, hospedagem) →
escalar_agentforce(tour_personalizado|orcamento, case_vendas_json=briefing+produtos_catalogo) → relay.
5. Contrato com o Agentforce¶
Chamadas (conforme a DAS)¶
| # | Chamada | Quando o Trip faz |
|---|---|---|
| 1 | POST https://{my_domain}.my.salesforce.com/services/oauth2/token (client_credentials) |
Na 1ª chamada, e de novo quando o token vence ou a API responde 401 (1 retry) |
| 2 | POST https://api.salesforce.com/einstein/ai-agent/v1/agents/{agent_id}/sessions |
No handoff: 1 sessão por handoff |
| 3 | POST .../sessions/{session_id}/messages |
seq 1 = contexto do Trip; seq 2..n = mensagens do cliente |
| 4 | DELETE .../sessions/{session_id} (x-session-end-reason) |
Inatividade (Timeout), Escalation (Transfer) |
externalSessionKey={session_id do Trip}:{uuid8}, rastreável até a conversa no Trip.bypassUser: true,streamingCapabilities.chunkTypes: ["Text"](modo síncrono; streaming fica para depois).
Primeira mensagem (seq 1): contexto do Trip¶
[CONTEXTO DO TRIP — handoff automático do assistente do site iFriend]
Motivo do handoff: produto_nao_encontrado
Canal: site
Conta: Agência (B2B) — nova (não encontrada na iFriend)
Empresa: Agência Viaje Bem | CNPJ: 11222333000181
Contato: Ana Souza | ana@viajebem.com.br | 5511999990000
Resumo: Procura passeio de balão na Capadócia para 2 pessoas em maio
Case: Cotação — Capadócia — 2 pax — 05/2026
Destino: Capadócia
Início da viagem: 2026-05-10
Adultos: 2
Produto de interesse: passeio de balão
Passagens compradas: sim
Hospedagem reservada: não
(Case completo, pronto para upsert, na variável de contexto Sales_Case_Payload)
A partir da próxima mensagem você conversa diretamente com o cliente
(as mensagens dele serão repassadas pelo Trip).
Variáveis de contexto (variables)¶
| Variável | Enviada | Conteúdo |
|---|---|---|
Sales_Case_Payload (env AGENTFORCE_CASE_VARIABLE) |
Sempre (vazio na env = desliga) | Corpo do Composite Upsert do Case (seção 3) |
| Variáveis avulsas (opcional) | Só as mapeadas em AGENTFORCE_CONTEXT_VARIABLES |
Ver tabela abaixo |
Mapeamento opcional de variáveis avulsas, configurável sem deploy de código:
AGENTFORCE_CONTEXT_VARIABLES='{"handoff_reason":"Handoff_Reason","customer_name":"Customer_Name",...}'
| Chave no Trip | API name proposto | Conteúdo |
|---|---|---|
trip_session_id |
Trip_Session_Id |
ID da conversa no Trip |
handoff_reason |
Handoff_Reason |
produto_nao_encontrado / pedido_humano / tour_personalizado / orcamento |
account_type |
Account_Type |
viajante / agencia |
customer_name |
Customer_Name |
Nome |
customer_email |
Customer_Email |
|
customer_phone |
Customer_Phone |
Telefone/WhatsApp |
summary |
Summary |
Resumo da conversa |
channel |
Channel |
sse / webchat |
whitelabel |
Whitelabel |
URL do whitelabel (quando aplicável) |
Como o Trip interpreta as respostas¶
type da mensagem |
Ação do Trip |
|---|---|
Inform |
Mostra o texto ao cliente e segue no relay |
SessionEnded |
Mostra o último texto (se houver) e sai do relay; o Trip volta a atender |
Escalation |
Sai do relay, faz o DELETE da sessão e orienta o cliente a pedir atendente (formulário de WhatsApp) |
Failure |
Registra em log e ignora |
6. Ciclo de vida do relay e fallback¶
Estado em session.state["agentforce"]:
{
"active": true, "failed": false,
"session_id": "sf-...", "external_key": "trip-sess:ab12cd34", "seq": 3,
"reason": "produto_nao_encontrado", "started_at": 0, "last_activity": 0,
"pending_reply": null, "last_relayed_invocation_id": "..."
}
| Evento | Resultado |
|---|---|
| Handoff aberto | active=true; a resposta ao contexto é exibida na mesma resposta do Trip |
| Mensagem do cliente com relay ativo | Repassada (seq+1); o LLM do Trip não roda |
SessionEnded |
active=false |
Sem mensagens por AGENTFORCE_IDLE_MINUTES (30) |
DELETE (Timeout), active=false; a mensagem atual segue para o Trip |
| Erro na Agent API durante o relay (4xx/5xx/timeout/circuit breaker) | active=false, failed=true; o cliente recebe um aviso e, ao pedir atendente, recebe o formulário de WhatsApp |
| Erro ao abrir o handoff | Fallback imediato para o formulário de WhatsApp (comportamento anterior) |
| Arquivo/mídia durante o relay | Envia um aviso textual ao Agentforce (a Agent API está configurada só para texto) |
Proteções do cliente HTTP: cache de token, 1 retry em 401, circuit breaker (3 falhas → 60s aberto),
timeout de AGENTFORCE_TIMEOUT (60s).
7. Implementação no Trip¶
Por que tool + callback, e não um novo sub-agente:
- Relay determinístico. No relay quem fala é o Agentforce. Um sub-agente LLM reescreveria cada turno, com mais latência e custo e com risco de distorcer a qualificação.
- O contexto já está nos agentes que disparam o handoff. Transferir para outro agente perderia o contexto
estruturado, e
disallow_transfer_to_peers=Truebloqueia a transferência entre irmãos. - Mesmo padrão já usado pelo
gerar_formulario_suporte.
| Arquivo | Papel |
|---|---|
ifriend_agent/integrations/agentforce/client.py |
AgentforceClient: OAuth, sessões, mensagens, DELETE, 401 retry, circuit breaker |
ifriend_agent/integrations/agentforce/handoff.py |
Estado do relay, mensagem de contexto, variables, regras de canal/whitelabel |
ifriend_agent/tools/escalar_agentforce_tool.py |
Tool do handoff (identificação da conta, Case, gate de acesso, fallback) |
ifriend_agent/integrations/agentforce/account.py |
Tipo de conta pelo JWT, validação de CNPJ |
ifriend_agent/integrations/agentforce/sales_case.py |
Modelo SalesCase (pydantic), obrigatoriedade por motivo, subject/resumo |
ifriend_agent/callbacks/agentforce_relay_callback.py |
Relay no before_agent_callback do root_agent |
ifriend_agent/prompts/agentforce_handoff_prompt.py |
Trechos de prompt (support, discovery, custom_tour) |
examples/agentforce_handoff_cli.py |
CLI de teste contra a sandbox |
ifriend_agent/tests/agentforce/ |
Testes unitários |
8. Configuração e deploy¶
| Variável | Default | Descrição |
|---|---|---|
ENABLE_AGENTFORCE_HANDOFF |
false |
Liga o handoff via Agentforce |
AGENTFORCE_MY_DOMAIN_URL |
— | My Domain da org (URL do token) |
AGENTFORCE_INSTANCE_ENDPOINT |
instance_url do OAuth |
instanceConfig.endpoint |
AGENTFORCE_AGENT_ID |
— | ID 0Xx... do agente SDR |
AGENTFORCE_CLIENT_ID |
— | Consumer Key |
AGENTFORCE_CLIENT_SECRET |
— | Consumer Secret (Secret Manager) |
AGENTFORCE_TIMEOUT |
60 |
Timeout HTTP (s) |
AGENTFORCE_TOKEN_LIFETIME_MINUTES |
20 |
Cache do token (o 401 renova de qualquer forma) |
AGENTFORCE_IDLE_MINUTES |
30 |
Inatividade até encerrar a sessão |
AGENTFORCE_CHANNELS |
sse,webchat |
Canais com handoff via Agentforce |
AGENTFORCE_ALLOW_WHITELABEL |
false |
Whitelabel também escala ao SDR iFriend |
AGENTFORCE_CASE_VARIABLE |
Sales_Case_Payload |
Variável de contexto que recebe o corpo do Composite Upsert do Case (vazio = não envia) |
AGENTFORCE_CASE_RECORD_TYPE_ID |
012U4000001VcQDIA0 |
RecordTypeId do Case "Fluxo de Vendas" (conferir por org) |
AGENTFORCE_CONTEXT_VARIABLES |
— | Mapeamento para variables avulsas (seção 5) |
Checklist de deploy (stage):
- Criar o secret:
gcloud secrets create trip-ifriend-agentforce-client-secret-stage+ versão com o Consumer Secret (verruntime/scripts/create-secrets.sh). - Em
cloudbuild.stage.yaml, adicionarAGENTFORCE_CLIENT_SECRET=trip-ifriend-agentforce-client-secret-stage:latestao--set-secretse um--set-env-varscomENABLE_AGENTFORCE_HANDOFF,AGENTFORCE_MY_DOMAIN_URL,AGENTFORCE_AGENT_ID,AGENTFORCE_CLIENT_ID. Só depois do passo 1: um--set-secretsapontando para um secret inexistente quebra o deploy. - Validar com
examples/agentforce_handoff_cli.pyantes de ligar a flag. - Produção: External Client App próprio (DAS §10), com secret e Agent ID de produção.
9. Agentforce → Trip via A2A (WhatsApp)¶
No WhatsApp o primeiro atendimento é do Agentforce, que chama o Trip pelo servidor A2A de entrada
(JSON-RPC, JWT com ROLE_A2A_USER) para buscar produtos. O protocolo A2A não define onde vai o
contexto do chamador, então o Trip publica o próprio contrato no Agent Card
(/a2a/.well-known/agent-card.json): a extensão
https://theifriend.com/a2a/extensions/caller-context/v1, opcional.
Onde o Agentforce pode mandar o contexto, em ordem de precedência:
- DataPart em
message.parts({"kind": "data", "data": {...}}). Preferido. message.metadata, com as chaves soltas ou aninhadas sob a URI da extensão.params.metadata.
{
"jsonrpc": "2.0", "id": "1", "method": "message/send",
"params": {
"message": {
"role": "user", "messageId": "…", "contextId": "sessao-agentforce-123",
"parts": [
{"kind": "text", "text": "Cliente quer passeio em Lisboa dia 12/11"},
{"kind": "data", "data": {
"caller": "agentforce", "caseId": "500…", "contactId": "003…",
"canal": "WhatsApp", "idioma": "pt-BR",
"cliente": {"nome": "Maria", "email": "maria@x.com", "telefone": "5511999990000"},
"conta": {"tipo": "viajante", "existente": true}
}}
]
}
}
}
Como o Trip trata essa chamada:
- O contexto é normalizado (aceita
caseId/case_id,channel/canal,language/idioma...) e gravado emsession.state["a2a_inbound_context"]antes do agente rodar. - O LLM vê só uma linha de resumo ("[Contexto do solicitante (via A2A): cliente Maria, canal WhatsApp, idioma pt-BR]"), e não o JSON bruto.
- Payload no estilo protobuf/A2A v1 (
"role": "ROLE_USER",content, parts semkind, métodosSendMessage/SendStreamingMessage) é normalizado para o formato v0.3 antes da validação. - Sem handoff quando o chamador é outro agente. O Trip não pede de novo os dados que já vieram e não escala para humano. Se não houver produto, responde isso claramente, e o Agentforce decide.
- Sem contexto nenhum, o Trip funciona só com o texto.
Detalhes para parceiros: Integração A2A → Para Parceiros.
10. Pontos em aberto (para a Everymind)¶
- Variável
Sales_Case_Payload: criar no agente. Ela recebe o corpo do Composite Upsert do Case, já com os API names (seção 3). Falta confirmar quem executa o upsert (Agentforce/Flow) e se oRecordTypeId012U4000001VcQDIA0é o mesmo em sandbox e produção. - API names das variáveis avulsas (seção 5), se forem usadas.
- Sinal da decisão final. Como o agente indica "Case/oportunidade aberta" vs "sem produto"? Hoje o
Trip depende de
SessionEnded. O agente vai encerrar a sessão ao decidir, ou deve devolver uma mensagem estruturada/variável? Escalationvia Agent API. Quando o agente quiser transferir para humano (Omni-Channel), o que acontece numa sessão da Agent API? O Trip hoje encerra a sessão e oferece o WhatsApp.- My Domain e Agent ID de produção, e confirmação de qual org responde pelo token e pelo
instanceConfig.endpoint(na collection da sandbox são hosts diferentes). - Deduplicação de conta/Case quando o mesmo cliente vier depois pelo WhatsApp (e-mail, telefone ou CNPJ como chave?).
- A2A (WhatsApp): confirmar se o Agentforce envia o contexto como DataPart (seção 9) e em qual versão do protocolo. Vale capturar uma requisição real no sandbox.
11. Segurança¶
- Credenciais (
client_id/client_secret) só no Secret Manager / variáveis secretas do Postman — nunca em código, docs ou collections exportadas. Um External Client App por ambiente. - Nada do Agentforce chega ao LLM do Trip além do texto das respostas. O contexto enviado contém nome, e-mail e telefone informados pelo próprio cliente para o atendimento.
- O Trip nunca menciona "Agentforce", "Salesforce" ou "SDR" ao cliente, só "nosso especialista".