CodeGym /Cursos /ChatGPT Apps /Tolerância a falhas: rollback de etapas, retries, control...

Tolerância a falhas: rollback de etapas, retries, controle de erros

ChatGPT Apps
Nível 11 , Lição 3
Disponível

1. Por que, no ChatGPT App, erros são a norma, não uma emergência

Na aula passada falamos sobre como dividir a tarefa em etapas e construir um workflow de múltiplas etapas no ChatGPT App. Agora vamos adicionar a essa estrutura a realidade nua e crua: erros, timeouts e interrupções pelo usuário.

Na web clássica, a lógica muitas vezes é construída em torno do “caminho feliz”, e os erros são percebidos como algo raro e emergencial — uma página vermelha 500 etc. No ChatGPT App, o cenário é outro: você trabalha em um sistema distribuído com LLM, APIs externas, MCP, um widget e, além disso, com um usuário que pode fechar a aba a qualquer momento. Erros e interrupções são rotina diária.

Há algumas particularidades que complicam a vida:

  • Em primeiro lugar, a LLM é não determinística. Mesmo com o mesmo prompt, ela pode tomar uma decisão um pouco diferente: chamar outra ferramenta, mudar parâmetros ou até decidir que é melhor “perguntar de novo”.
  • Em segundo lugar, há limitações de rede e infraestrutura. A chamada de ferramenta (tool‑call) do ChatGPT tem timeouts (geralmente dezenas de segundos), assim como seu backend em Next.js/Vercel. Se a API externa estiver lenta, tudo pode cair pela metade.
  • Em terceiro lugar, há o fator de UX: o usuário se distraiu, fechou o chat, voltou depois de um dia, e você não tem como manter uma transação aberta no banco durante todo esse tempo.

Daqui, a tese principal da aula:

Workflow tolerante a falhas = um cenário em que assumimos antecipadamente que qualquer etapa pode falhar e definimos explicitamente o que acontece então.

Um erro não é apenas motivo para mostrar uma mensagem ao usuário, mas também um sinal para o modelo, que pode mudar a estratégia, sugerir rollback, tentar outra ferramenta ou finalizar o cenário com cuidado.

2. Panorama de erros no workflow: quais existem

Para tratar falhas corretamente, primeiro é preciso saber diferenciá‑las. Em um aplicativo LLM baseado em ChatGPT Apps, geralmente encontramos diversas classes de erros.

Erros técnicos. É toda a clássica lista de sistemas distribuídos: timeouts de rede, 5xx das suas APIs ou de APIs externas, queda do servidor MCP, bug no código do handler da ferramenta. Por exemplo, no GiftGenius sua ferramenta MCP search_products consulta o catálogo, e ele responde 503 Service Unavailable. É um candidato a retry automático.

Erros lógicos (do modelo). Incluem recusas do modelo (ele decidiu que a solicitação viola a política), alucinações ou, por exemplo, JSON corrompido na resposta da ferramenta. O modelo pode ter gerado argumentos incorretos para o tool‑call, e sua validação JSON não os aceitou. Na maioria das vezes isso é erro de entrada de dados, não de infraestrutura.

Erros de negócio. Tratam do significado: o produto esgotou, o orçamento do usuário é pequeno demais para os filtros escolhidos, o cupom é inválido, a reserva já expirou. No GiftGenius, é a situação “de 500 candidatos, nenhum atende às restrições definidas”. Retry raramente ajuda aqui: é preciso mudar os parâmetros ou explicar ao usuário que a restrição é irrealista.

Interrupções de UX. O próprio usuário interrompe o cenário: fecha o ChatGPT, clica em “Voltar” no widget, cancela uma ação, muda a resposta da etapa anterior. Isso também deve ser considerado um fluxo normal, não um erro. É importante restaurar e fazer rollback do estado nesses casos — falaremos disso em seguida.

Um caso problemático à parte, na interseção entre erros lógicos e técnicos, são os ciclos infinitos do agente: o modelo recebe um erro, pensa “hmm, vou tentar de novo”, dá erro de novo — e assim por diante até acabar o contexto ou o orçamento. Proteger‑se desse comportamento é parte importante do design de erros.

3. Estratégias básicas: retry, fail‑fast, rollback e envolvimento do usuário

Qualquer erro pode ser visto como um ponto de ramificação: ou tentamos repetir a etapa, ou fazemos rollback, ou envolvemos o usuário. E, importante, essas estratégias se combinam.

Para falhas técnicas e temporárias (rede oscilou, a API retornou 503), faz sentido fazer retry limitado com backoff. Para erros lógicos e de negócio (“o validador não aceitou o orçamento”, “os produtos acabaram”), repetir é inútil; precisamos de fail‑fast e pedir ao usuário que altere a entrada ou os parâmetros.

Para operações que já mudaram algo no mundo externo (pedido criado, reserva feita), é necessário rollback — seja como um “passo para trás” no UI/contesto, seja na forma de ações compensatórias reais (cancelar pedido, reembolsar).

Por fim, há casos que exigem participação do usuário: por exemplo, se o sistema de pagamento recusar por “cartão recusado pelo banco”, você não pode consertar isso automaticamente. O modelo deve explicar corretamente o que aconteceu e sugerir opções: tentar outro cartão, reduzir o valor ou desistir da compra.

Para um workflow confiável, é muito útil listar, para cada etapa, quais tipos de erro são possíveis e o que você faz em cada um deles — auto‑retry, rollback, solicitação ao usuário ou apenas log e término do ramo.

4. Retries e backoff: quando e como fazê‑los

Vamos começar com a reação mais natural do desenvolvedor: “Vamos só tentar mais uma vez”. A ideia é correta, mas, como sempre, o diabo mora nos detalhes.

Quais erros vale a pena repetir (retry)

Uma boa heurística, vinda da prática de integrações, é: erros de rede e 5xx podem ser tentados novamente com uma pausa; já 4xx — provavelmente não.

Ou seja, se você recebeu 503, 504 ou simplesmente não obteve resposta de uma API externa, repetir a solicitação com um pequeno atraso faz sentido. Se o servidor retornou 400 Bad Request ou 422 Unprocessable Entity, é mais provável que o problema esteja nos dados, e repetir com os mesmos parâmetros não vai mudar nada.

Utilitário simples callWithRetry em TypeScript

Vamos escrever um pequeno utilitário para a camada MCP ou backend, que pode ser usado nas ferramentas:

type RetryOptions = {
  maxRetries: number;
  baseDelayMs: number;
};

async function callWithRetry<T>(
  fn: () => Promise<T>,
  { maxRetries, baseDelayMs }: RetryOptions
): Promise<T> {
  let attempt = 0;

  // não queremos loops infinitos
  while (true) {
    try {
      return await fn();
    } catch (err: any) {
      attempt++;
      const status = err?.status ?? err?.response?.status;

      // não fazemos retry em 4xx
      const isClientError = typeof status === "number" && status >= 400 && status < 500;
      if (attempt > maxRetries || isClientError) {
        throw err;
      }

      const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), 10_000);
      // pequena pausa para evitar efeito manada na API
      const jitter = Math.random() * 200;

      await new Promise((r) => setTimeout(r, delay + jitter));
    }
  }
}

Esta função:

  • repete a chamada de fn um número limitado de vezes;
  • usa backoff exponencial com um pequeno ruído aleatório (jitter), para evitar o efeito manada quando vários retries ocorrem ao mesmo tempo;
  • interrompe os retries em 4xx.

É útil empregá‑la, por exemplo, dentro de uma ferramenta MCP que consulta o catálogo de produtos ou uma API interna de recomendações.

Onde exatamente fazer retry

Um erro comum é tentar repetir todas as solicitações em sequência, inclusive em camadas que você não controla. No ecossistema do ChatGPT, há vários lugares para retries:

  • no seu próprio backend/MCP (como fizemos em callWithRetry);
  • dentro de um worker/fila em background (em módulos futuros vamos falar mais sobre filas de jobs e DLQ);
  • às vezes — no próprio widget, quando se trata de uma solicitação leve do tipo “atualizar lista”, sem efeitos colaterais.

É importante não duplicar a lógica: se seu worker de jobs já faz três retries com backoff, não faz sentido adicionar mais cinco retries no widget. E, claro, nunca faça while(true) { try ... } — é um jeito certo de dar DDoS em si mesmo.

5. Idempotência das etapas: proteção contra duplicatas

Retries criam um segundo problema: como não executar a mesma ação duas vezes. No mundo LLM isso é especialmente crítico: o modelo pode, por engano, chamar a mesma ferramenta mais de uma vez, o ChatGPT pode repetir o tool‑call após timeout, o usuário pode clicar em “Regenerate”, e então o UI ou o agente podem, por conta própria, adicionar outra chamada.

A ideia de idempotência é simples: uma etapa é considerada idempotente se sua execução repetida com os mesmos dados de entrada não cria efeitos colaterais adicionais. Solicitar o product feed — ok, recalcular recomendações — ok, mas cobrar novamente ou criar um segundo pedido com os mesmos dados — definitivamente não.

Idempotency key no ChatGPT App

Um padrão clássico: para cada etapa lógica com efeitos colaterais, você gera um idempotency_key (geralmente um UUID), o transmite pelo modelo até a ferramenta MCP e armazena, lá, a correspondência “chave → resultado”. Se a ferramenta for chamada uma segunda vez com a mesma chave, ela não repete a ação, apenas retorna o resultado já salvo.

No nosso GiftGenius, há a etapa create_order. Imagine que o usuário clicou no botão “Pagar”, o modelo chamou a ferramenta, o pagamento foi bem‑sucedido, mas a resposta se perdeu no caminho. O modelo ou a plataforma decidem repetir a chamada e, se não houver idempotência, teremos pedido duplicado ou cobrança dupla.

Exemplo simples de ferramenta idempotente em TypeScript

Vamos fazer um handler bem simplificado da ferramenta MCP create_order com uma chave de idempotência. Para simplificar, usaremos um Map em memória; na vida real, seria um banco ou cache.

type CreateOrderInput = {
  userId: string;
  items: Array<{ sku: string; qty: number }>;
  idempotencyKey: string;
};

type CreateOrderResult = { orderId: string; status: "created" };

const idempotencyStore = new Map<
  string,
  { paramsHash: string; result: CreateOrderResult }
>();

export async function createOrderTool(input: CreateOrderInput): Promise<CreateOrderResult> {
  const { idempotencyKey, ...rest } = input;
  const paramsHash = JSON.stringify(rest);

  const existing = idempotencyStore.get(idempotencyKey);
  if (existing) {
    // se a chave já existia, garantimos que os parâmetros coincidem
    if (existing.paramsHash !== paramsHash) {
      throw new Error("Idempotency key reuse with different params");
    }
    return existing.result;
  }

  // aqui fazemos a criação real do pedido e a cobrança
  const result: CreateOrderResult = {
    orderId: "order_" + Math.random().toString(36).slice(2),
    status: "created",
  };

  idempotencyStore.set(idempotencyKey, { paramsHash, result });
  return result;
}

Aqui nós:

  • exigimos idempotencyKey nos dados de entrada da ferramenta;
  • armazenamos com ela o hash dos parâmetros (aqui, para simplificar, JSON.stringify);
  • em chamada repetida com a mesma chave, mas dados diferentes — consideramos erro;
  • em chamada repetida com os mesmos dados — apenas retornamos o resultado anterior.

No projeto real, vale a pena:

  • armazenar as chaves no banco com TTL (para a tabela não crescer sem limite);
  • registrar em log a idempotency_key e incluí‑la em _meta das mensagens MCP, para acompanhar facilmente pelo Inspector e dashboards.

6. Rollback de etapas e o padrão Saga

Idempotência protege contra duplicatas, mas não resolve outro caso: o que fazer se uma das etapas no meio do cenário falhar.

Em e‑commerce, este é um problema clássico: você já criou o pedido e reservou o item no estoque, mas algo deu errado na etapa de pagamento. Você não pode simplesmente “esquecer” — precisa reverter o estado anterior.

Rollback lógico vs. técnico

No workflow do ChatGPT há dois níveis de rollback.

Rollback lógico — é voltar à etapa anterior do cenário e ajustar o contexto. Por exemplo, ocorreu um erro na etapa de “pagamento”, e você decide voltar para “escolha do método de pagamento” ou até “escolha do presente”. Então é importante:

  • atualizar o WorkflowContext no backend (etapa atual, parâmetros escolhidos);
  • informar o modelo sobre a troca de etapa via tool‑call/ToolOutput, para que ele “esqueça” o ramo antigo e ajuste o comportamento;
  • atualizar o UI do widget para que as etapas e botões reflitam o novo estado.

Rollback técnico — é o nível de negócio: cancelar entidades criadas, compensar efeitos externos. Por exemplo: cancelar o pedido, remover a reserva do estoque, iniciar reembolso. Este é o padrão Saga: para cada etapa “perigosa”, você planeja uma ação compensatória.

Esquema forward/compensate para o GiftGenius

Para um checkout simplificado do GiftGenius, podemos desenhar a sequência:

flowchart TD
  A[Etapa 1: create_order] --> B[Etapa 2: reserve_items]
  B --> C[Etapa 3: charge_card]

  C -->|sucesso| D[Status: completed]

  C -->|erro| E[Compensação: cancel_reservation]
  E --> F[Compensação: cancel_order]
  F --> G[Status: failed + mensagem para o usuário]

Cada ação que muda o mundo externo (criação de pedido, reserva, pagamento) tem uma ação compensatória correspondente (cancelar pedido, remover reserva, reembolsar). Elas nem sempre são simétricas nem possíveis 1:1, mas o princípio geral é esse.

Miniexemplo com compensação em código

Vamos ver um pequeno trecho que executa essas etapas:

async function completeCheckout(ctx: { userId: string }) {
  const order = await createOrderInDb(ctx.userId);

  try {
    await reserveItems(order.id);
    await chargeCard(order.id);
    return { orderId: order.id, status: "paid" as const };
  } catch (err) {
    // ações de compensação
    await safeCancelReservation(order.id);
    await safeCancelOrder(order.id);
    throw err;
  }
}

Aqui:

  • createOrderInDb, reserveItems, chargeCard — etapas forward;
  • safeCancelReservation e safeCancelOrder — etapas de compensação, que por si só devem ser idempotentes (se tentarmos cancelar algo já cancelado, nada de ruim acontece).

Observe que, em caso de erro, não o escondemos, e sim propagamos. O modelo (via ToolOutput) deve receber uma mensagem de erro clara, explicar isso ao usuário em linguagem humana e propor o próximo passo.

7. Rollback de etapas e sincronização de estado: como evitar dessincronização

Há um tipo especial de “erro” que é fácil subestimar: a dessincronização de estado entre UI, backend e modelo.

Cenário típico:

  1. O usuário passa pelas etapas 1 → 2 → 3.
  2. Na etapa 3 algo dá errado; o usuário clica no botão “Voltar” no widget.
  3. O widget honestamente volta seu estado local para a etapa 2.
  4. Mas o modelo “lembra” que estávamos na etapa 3 e já tentamos pagar. Na próxima mensagem, ele continua falando de pagamento, embora o usuário veja a tela de escolha do presente.

Para evitar isso, é útil introduzir um evento explícito de rollback de etapa. O widget o envia ao MCP/modelo — como uma chamada de ferramenta ou como ToolOutput.

Por exemplo, podemos criar uma ferramenta simples user_navigated_to_step, que fixa a etapa atual e seu estado:

type NavigateInput = {
  workflowId: string;
  stepId: string;
};

export async function userNavigatedToStep(input: NavigateInput) {
  await workflowRepo.setCurrentStep(input.workflowId, input.stepId);
  return {
    message: `User moved to step ${input.stepId}`,
  };
}

O widget, ao clicar em “Voltar”, chama essa ferramenta; o modelo vê o resultado no histórico de tool‑calls e entende que agora deve continuar o diálogo a partir da nova etapa.

No lado do UI, o handler fica mais ou menos assim:

async function handleBackClick() {
  const { workflowId, prevStepId } = widgetState;

  await window.openai.tools.call("user_navigated_to_step", {
    workflowId,
    stepId: prevStepId,
  });

  setWidgetState((s) => ({ ...s, currentStepId: prevStepId }));
}

Ponto importante: o backend/agente é a fonte da verdade sobre a etapa atual, e o modelo a enxerga por meio das ferramentas. Assim, mesmo ao restaurar a sessão mais tarde, você consegue sincronizar o contexto corretamente.

8. UX de erros: o que o usuário vê e o que o modelo vê

Já aprendemos a sobreviver tecnicamente aos erros (retries, rollbacks, idempotência, sincronização de estado). Falta fazer isso parecer adequado tanto para o usuário quanto para o modelo.

Mesmo um retry e rollback perfeitos não salvam se o UX de erros for “como nos velhos servlets Java”: texto vermelho, stack trace e o enigmático “Unexpected error”.

Para o ChatGPT App, há dois públicos para a mensagem de erro:

  • o usuário, que precisa entender o que aconteceu e o que pode fazer em seguida;
  • o modelo, que precisa receber informação suficientemente estruturada para decidir: repetir, mudar parâmetros, oferecer alternativa ou encerrar o cenário.

Boa prática:

  • no nível de MCP/ferramentas, retornar um erro estruturado com código, tipo, flag retryable e um texto técnico curto;
  • dar ao modelo exatamente essa estrutura (por exemplo, em result.structuredContent), não um quilômetro de stack trace;
  • no UI, mostrar ao usuário uma mensagem humana e curta.

Miniexemplo de estrutura de erro retornada pela ferramenta:

type ToolError = {
  code: string;          // e.g. "PAYMENT_TIMEOUT"
  message: string;       // breve descrição técnica
  retryable: boolean;    // se vale tentar novamente
};

throw {
  isError: true,
  error: <ToolError>{
    code: "PAYMENT_TIMEOUT",
    message: "Payment provider did not respond in time",
    retryable: true,
  },
};

O modelo vê retryable: true e pode tentar outra ferramenta ou sugerir ao usuário repetir a tentativa.

No widget, você apenas mapeia esses códigos para textos compreensíveis ao usuário:

function ErrorBanner({ code }: { code: string }) {
  const text =
    code === "PAYMENT_TIMEOUT"
      ? "O serviço de pagamento não respondeu a tempo. Tente novamente em um minuto."
      : "Algo deu errado. Por favor, tente novamente.";

  return <div className="error-banner">{text}</div>;
}

E mais um ponto importante: não mostre ao usuário stacks de exceções, tokens, segredos. Isso é feio e inseguro. As informações técnicas você registra nos seus logs; ao usuário, entregue uma mensagem curta e segura.

Insight

Em sistemas de LLM como o ChatGPT, chamadas incorretas de ferramentas são mais a norma do que a exceção. O modelo gera regularmente argumentos que não passam na validação: tipos trocados, campos ausentes, valores inválidos, estruturas quebradas. Isso não é um erro no sentido de engenharia tradicional — é parte da natureza estocástica do modelo, e todo o interfaceamento de erros precisa se adaptar a isso.

A ideia‑chave: a mensagem de erro não é um sinal de “quebrou”, mas uma instrução para corrigir a próxima tentativa. Seu principal público é o próprio modelo. Se a mensagem for estruturada e contiver instruções precisas, o modelo consegue ajustar os parâmetros automaticamente e repetir a chamada corretamente. Esse é exatamente o princípio em que se baseiam técnicas de Tool‑Reflection: feedback correto melhora a próxima ação do agente sem intervenção humana.

Recomendo seguir estes requisitos para o formato dos erros:

  • a mensagem deve indicar o campo específico que falhou na validação — sem generalidades do tipo “Invalid parameters”;
  • é importante descrever explicitamente o formato esperado ou valores permitidos, para que o modelo possa escolher o adequado;
  • a mensagem deve ser curta, formal e estruturada: campos como error_type, field, expected ou allowed_values ajudam muito o modelo;
  • quando possível, forneça um exemplo mínimo de entrada correta — isso costuma aumentar a precisão da recuperação do modelo.

O feedback de erro ideal para o modelo contém dois fatos: o que deu errado e instruções de como corrigir.

9. Logs e métricas de erros do workflow

Mesmo que o UX de erros esteja caprichado, para entender o que realmente quebra no seu sistema, mensagens ao usuário não bastam. São necessários logs estruturados e métricas por etapa.

Conjunto mínimo útil ao registrar cada etapa do workflow:

  • user_id ou pelo menos session_id;
  • workflow_id e step_id;
  • status da etapa (success, failed, retry, rolled_back);
  • error_code (se houve);
  • idempotency_key e correlation_id, se a etapa estiver ligada a chamadas externas.

Em MCP e Agents há campos _meta; é conveniente colocar ali idempotency_key e correlation_id, para vê‑los tanto nos logs quanto no Inspector.

Exemplo mais simples de logging em Node.js/TypeScript (pode usar console, ou winston/pino):

function logStepFailure(params: {
  userId?: string;
  workflowId: string;
  stepId: string;
  errorCode: string;
  idempotencyKey?: string;
}) {
  console.error(
    JSON.stringify({
      level: "error",
      event: "workflow_step_failed",
      ...params,
      timestamp: new Date().toISOString(),
    })
  );
}

Esses logs são fáceis de parsear, construir dashboards e calcular:

  • a conversão entre etapas;
  • os tipos de erro mais frequentes;
  • a proporção de etapas que terminaram em retry vs. falha definitiva.

Nem todo erro deve virar alerta em produção. Críticos — queda do MCP, timeouts sistemáticos, falhas em massa em uma determinada etapa — sim, devem ir para o monitoramento. Mas “sem resultados na busca de presentes” — isso é um evento de negócio, não um incidente.

10. Evoluindo o GiftGenius: etapa de checkout resiliente

Agora vamos juntar tudo: retries, idempotência, Saga, sincronização de estado, UX de erros e logging — no exemplo de uma etapa do nosso app didático GiftGenius — a finalização do pedido.

O que já temos

Neste ponto, já temos:

  • um workflow de múltiplas etapas: coleta de informações → seleção de ideias → escolha do presente → checkout;
  • tool gating configurado: na etapa de checkout, apenas o conjunto de ferramentas de comércio está disponível (create_order, get_payment_methods etc.);
  • um WorkflowContext com o presente escolhido, o orçamento, o userId e a etapa atual.

O que vamos adicionar nesta aula

Para a etapa de checkout, vamos implantar:

  1. idempotency_key na ferramenta create_order;
  2. retry em erros temporários do provedor de pagamento;
  3. compensação em operações parcialmente bem‑sucedidas;
  4. UX de erros adequado no widget.

Geração da chave de idempotência no widget ao clicar em “Pagar”:

import { v4 as uuid } from "uuid";

async function handlePayClick() {
  const idempotencyKey = uuid();
  setWidgetState((s) => ({ ...s, idempotencyKey }));

  await window.openai.tools.call("create_order", {
    userId: widgetState.userId,
    items: [/* ... */],
    idempotencyKey,
  });
}

No lado da ferramenta create_order — aquele handler idempotente que escrevemos acima: ele armazena a chave e o resultado e, ao repetir, não cria um novo pedido.

O código de interação com a API de pagamentos pode ser encapsulado em callWithRetry, para tentar a cobrança algumas vezes em caso de falhas de rede. E não se esqueça de adicionar o flag retryable: true no erro, para que o modelo entenda que pode sugerir repetir.

Se, após a criação bem‑sucedida do pedido e a cobrança, algo quebrar (por exemplo, um webhook externo não chegar a tempo), registramos isso com correlation_id e workflow_id e então:

  • tentamos um retry em background (em um módulo futuro sobre filas e eventos);
  • ou marcamos explicitamente a etapa como failed, chamamos as ações compensatórias e explicamos ao usuário o que aconteceu.

11. Erros comuns ao projetar workflows tolerantes a falhas

Erro nº 1: “Fazer retry de tudo até funcionar”.
Repetir automaticamente qualquer etapa até dar certo é um caminho certeiro para o caos local. Erros de rede e 5xx podem ser tentados de novo com backoff e limite de tentativas. Mas 4xx, erros de negócio e falhas lógicas do modelo precisam ser corrigidos com dados ou explicados ao usuário. Caso contrário, você terá comportamento instável, contas estranhas e logs poluídos.

Erro nº 2: Falta de idempotência em etapas de dinheiro e pedidos.
Se uma ferramenta como create_order ou charge_card não for idempotente, qualquer chamada repetida (por timeout, Regenerate, bug no agente) pode gerar duplicatas. Em cenários LLM, retries são muito mais frequentes do que no frontend REST clássico, então idempotency_key não é “bônus legal”, é requisito obrigatório para etapas críticas de pagamento e afins.

Erro nº 3: Sem ações de compensação (ausência de Saga).
Criou o pedido, reservou o item, mas falhou no pagamento e só mostrou ao usuário “algo deu errado”. Resultado: ficam pendurados semipedidos, reservas, “rabos” financeiros. Para cada etapa que muda o mundo externo, pense no que fazer se a próxima falhar: cancelar, reembolsar, marcar como “expired” etc.

Erro nº 4: Deixar o agente entrar em um ciclo infinito de retries.
Se você não limitar a quantidade de tentativas (por exemplo, com maxRetries em helpers ou max_iterations na lógica do agente) e não marcar erros como retryable: false onde retry é inútil, o modelo pode entrar em loop: “Vou tentar de novo… de novo…”. Isso queima tokens, tempo e paciência.

Erro nº 5: Dessincronização de estado entre UI e modelo no rollback.
Muitas vezes os devs implementam o botão “Voltar” apenas no UI, esquecendo de sincronizar a etapa com backend e modelo. No fim, o usuário vê a etapa 2, enquanto o modelo continua “na etapa 3” e faz sugestões esquisitas. A solução — eventos explícitos como user_navigated_to_step e atualização do WorkflowContext a cada transição.

Erro nº 6: Mensagens técnicas para o usuário e falta de logs para os desenvolvedores.
O usuário recebe “Error: ECONNRESET at TcpSocket.onEnd…”, e você — zero informação sobre qual etapa e para qual workflow_id quebrou. Abordagem correta: para o usuário — texto curto, claro e uma sugestão do que fazer; para o desenvolvedor — log estruturado com workflow_id, step_id, error_code, idempotency_key e correlation_id.

Erro nº 7: Ausência de estratégia de alertas.
Ou alertam tudo, incluindo “não há presentes para seu filtro super restrito”, ou não alertam nada, incluindo a queda real do MCP. Procure separar falhas sistêmicas críticas (queda de serviço, timeouts em massa, perda de webhooks) de eventos de negócio esperados. Os primeiros vão para monitoramento e on‑call; os segundos — apenas entram na análise.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION