CodeGym /Cursos /ChatGPT Apps /Salvar e restaurar o contexto entre etapas

Salvar e restaurar o contexto entre etapas

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

1. O que é o contexto do workflow e por que ele é necessário

Em um aplicativo web comum, você tem uma ideia bem clara de onde vive o estado: banco de dados, cache e algo no front-end como Redux ou o estado local do React. No ChatGPT App é mais divertido: o estado fica espalhado por três mundos ao mesmo tempo — dentro do modelo (histórico do diálogo), dentro do widget (estado de UI) e no seu servidor/MCP (dados de negócio).

Por contexto do workflow vamos entender todo o conjunto de dados necessários para responder às perguntas “em que etapa estamos” e “o que já se sabe”. Falando do nosso app didático GiftGenius, o contexto inclui:

  • perfil do destinatário do presente: idade, gênero, interesses;
  • orçamento e, possivelmente, a moeda;
  • lista de ideias geradas e quais delas o usuário curtiu ou ocultou;
  • itens técnicos: identificador da sessão ou do workflow, status (“profile_collected”, “ideas_shown”, “checkout_started”).

Esse contexto é necessário não só para você como desenvolvedor de backend. Ele é necessário para o próprio modelo, para que entenda quais perguntas já foram feitas, quais ferramentas já foram chamadas e sobre o que estamos falando agora. E é necessário para o usuário, para que, ao voltar ao chat, não precise começar tudo do zero.

O usuário intuitivamente pensa que “o ChatGPT lembra de tudo”. Na prática, o modelo lembra apenas o texto do diálogo, e enquanto ele couber na janela de contexto. Coisas estruturadas como order_id, cart_id ou “lista de ideias curtidas” precisam ser armazenadas no seu servidor, do contrário você terá uma máquina perfeita para gerar afirmações confiantes, porém incorretas.

2. Três níveis de estado: UI, LLM e negócio

É mais fácil entender a preservação do contexto por meio de um modelo de três camadas de estado. Também chamado de “State Triad”.

Tabela de níveis

Vamos usar uma pequena tabela:

Nível Onde fica Ciclo de vida Responsabilidade Exemplo no GiftGenius
UI State Widget (React, widgetState) Enquanto o chat/mensagem com o widget está aberto Estado visual, entrada local Quais cartões estão destacados, estado do formulário
LLM Context Histórico do chat no OpenAI Enquanto a mensagem “cabe” no contexto Compreensão do diálogo e raciocínio “Estamos buscando um presente para a mãe, orçamento de $50”
Business State MCP / seu backend (BD/Redis) Quanto tempo você quiser (persistente) Fonte da verdade: dados verificados, status { step: "ideas", budget: 50, liked: [42, 51] }

A camada de UI é rápida e responsiva, mas muito frágil: o ChatGPT pode “desmontar” o iframe com o widget quando você rola para cima no histórico e depois montá-lo novamente. É para isso que existe o widgetState, que vive um pouco mais do que o componente React e é sincronizado com o host do ChatGPT.

A camada do LLM dá ao modelo a sensação de um diálogo contínuo, mas armazena apenas texto e chamadas de ferramentas. Você pode colocar ali um JSON com seu carrinho, mas isso é basicamente inserir o JSON no texto — o modelo não vai tratar isso como um banco de dados.

A camada de negócio é o que você, como engenheiro, pode controlar: lá ficam dados validados, índices e status de pedidos. Assim que você tiver um roteiro sério (presentes, reservas, ensino), essa camada deve se tornar a principal fonte da verdade sobre o estado.

O principal problema de engenharia é evitar que essas três camadas se desalinhem. O usuário mudou o orçamento no widget, o modelo ainda pensa no antigo e no banco há um terceiro valor — essa é a receita clássica para um comportamento estranho.

3. O que exatamente salvamos: a estrutura do WorkflowContext

Para falar de forma concreta, vamos descrever em TypeScript a interface de contexto do GiftGenius. Suponha que já temos algumas etapas: coleta de perfil, escolha de orçamento, geração de ideias e visualização/curtidas.

Comecemos com uma estrutura simples:

// backend/types/workflow.ts
export type GiftWorkflowStep =
  | "profile"
  | "budget"
  | "ideas"
  | "checkout";

export interface GiftWorkflowContext {
  id: string;              // workflowId — identificador do cenário
  userId?: string;         // se a autenticação já estiver configurada
  currentStep: GiftWorkflowStep;
  profile?: {
    age?: number;
    gender?: string;
    interests?: string[];
  };
  budget?: {
    min?: number;
    max?: number;
    currency: string;
  };
  ideas?: {
    id: string;
    title: string;
  }[];
  likedIdeaIds: string[];
  hiddenIdeaIds: string[];
  updatedAt: number;       // timestamp para TTL/limpeza
}

Não é um schema final, mas os elementos importantes já estão no lugar. Temos:

  • um identificador do workflow, pelo qual vamos buscar esse contexto;
  • a etapa atual, que ajudará tanto o widget quanto o modelo a entender até onde chegamos;
  • um conjunto de campos que serão preenchidos em etapas específicas;
  • campos de serviço como o horário da atualização.

Um comentário à parte sobre identificadores. Nesta aula, por workflowId entendemos o identificador de um roteiro específico dentro do nosso backend/MCP. Ele pode coincidir com o identificador da sessão de diálogo do ChatGPT (sessionId), mas não contamos com isso. userId é o identificador do usuário do seu sistema de autenticação (se houver); um usuário pode ter vários workflows ativos. No campo id está justamente esse workflowId, pelo qual buscamos e atualizamos o contexto.

Nas próximas seções, vamos analisar três coisas: onde armazenar esses objetos, como gravá-los lá e como recuperá-los — tanto no widget quanto no modelo.

4. Onde armazenar o estado: opções e compromissos

É útil pensar na persistência do estado em duas dimensões: onde ele é armazenado e por quanto tempo vive. Nesta seção, vamos focar no local de armazenamento e voltar aos prazos de vida no checklist e no bloco de erros comuns.

Primeiro, vamos entender os locais de armazenamento.

Dentro do diálogo (no prompt)

Às vezes dá vontade de dizer: “Vamos simplesmente devolver à modelo, toda vez, um JSON com o estado atual e deixá-la se virar”. Isso funciona para roteiros muito simples e cadeias curtas de etapas, mas esbarra rapidamente em dois problemas: limite de tamanho do contexto e ausência de qualquer garantia de integridade dos dados.

Além disso, o protocolo MCP é, por natureza, stateless: como o HTTP, ele não mantém estado entre requisições por padrão. Para vincular a chamada de uma ferramenta a uma sessão específica, você precisa passar explicitamente um identificador — o workflowId ou session id — nos argumentos da ferramenta ou por metadados/cabeçalhos.

Portanto, manter o estado de negócio apenas no diálogo é mais um experimento didático do que uma arquitetura.

No widget: UI + widgetState

No nível da UI usamos o estado usual do React (useState, useReducer e assim por diante), mas, como já foi dito, o componente pode ser desmontado. No Apps SDK existe o mecanismo widgetState, que vive fora do React e é sincronizado com o host do ChatGPT. Se, ao montar o widget, você buscar o valor salvo e, ao ocorrerem mudanças, salvá-lo de volta, você terá um armazenamento local, porém bem conveniente.

Esse armazenamento é ótimo para estado puramente visual: quais cartões estão recolhidos, em qual aba você está, o que o usuário digitou no formulário antes de clicar em “Avançar”. Mas ele não substitui o servidor: assim que o usuário abrir o chat em outro dispositivo ou depois de uma semana, o widgetState pode já não ajudar. E construir a lógica de negócio sobre ele — é discutível.

No servidor/MCP: Map, Redis, BD

Por fim, a opção principal para produção: armazenamos o GiftWorkflowContext no lado do servidor MCP ou de um serviço de backend. Como o cliente e o servidor MCP são stateless pelo protocolo, devemos encaminhar o workflowId (ou state_token) em cada chamada de ferramenta para entender qual contexto atualizar.

Há várias opções de implementação:

  • Map in-memory no Node.js — adequado para demos e ambiente de desenvolvimento: é tudo rápido, mas desaparece no restart;
  • Redis ou outro cache in-memory com TTL — bom para roteiros tipo wizard (assistentes de algumas etapas): vive por uma ou duas horas e depois pode ser removido;
  • um banco SQL/NoSQL comum — obrigatório para roteiros do tipo “voltei uma semana depois” ou “rascunhos e carrinhos”.

Nesta aula não vamos aprofundar em um banco específico; vamos focar na interface e no entendimento do que exatamente deve ir para lá.

5. O storage mais simples no servidor MCP: Map por workflowId

Vamos começar com algo simples: uma Map in-memory no servidor MCP, em que a chave é o workflowId. Em um demo didático, você pode simplesmente igualá-lo ao sessionId do diálogo, mas em produção é melhor manter o workflowId como um identificador separado do roteiro. O valor nessa Map será o GiftWorkflowContext. No mundo real, você trocará isso por Redis ou um BD, mas a API continuará a mesma.

Suponha que nosso servidor MCP seja em TypeScript. Adicione perto da inicialização:

// mcp/workflowStore.ts
import { GiftWorkflowContext } from "../backend/types/workflow";

const workflows = new Map<string, GiftWorkflowContext>();

export function getWorkflow(id: string): GiftWorkflowContext | undefined {
  return workflows.get(id);
}

export function saveWorkflow(ctx: GiftWorkflowContext): void {
  workflows.set(ctx.id, { ...ctx, updatedAt: Date.now() });
}

Em seguida, a ferramenta que salva o perfil do destinatário. Importante: ela recebe o workflowId e os dados do perfil e, internamente, atualiza/cria o contexto correspondente:

// mcp/tools/setProfile.ts
import { jsonSchema } from "@modelcontextprotocol/sdk"; // alias
import { getWorkflow, saveWorkflow } from "../workflowStore";

export const setProfileTool = {
  name: "gift_set_profile",
  description: "Salva o perfil do destinatário do presente",
  inputSchema: jsonSchema.object({
    workflowId: jsonSchema.string(),
    age: jsonSchema.number().optional(),
    gender: jsonSchema.string().optional(),
    interests: jsonSchema.array(jsonSchema.string()).optional()
  }),
  async run(input: any) {
    const existing = getWorkflow(input.workflowId);
    const ctx = existing ?? {
      id: input.workflowId,
      currentStep: "profile",
      likedIdeaIds: [],
      hiddenIdeaIds: []
    };
    ctx.profile = {
      age: input.age,
      gender: input.gender,
      interests: input.interests ?? []
    };
    ctx.currentStep = "budget";
    saveWorkflow(ctx);
    return {
      structuredContent: {
        type: "profileSaved",
        workflowId: ctx.id,
        profile: ctx.profile,
        nextStep: ctx.currentStep
      }
    };
  }
};

Essa ferramenta já resolve duas tarefas: salva o perfil e avança o currentStep para a próxima etapa. Em um projeto real, você talvez queira separar as ferramentas “salvar dados” e “ir para a etapa”, mas para entender o conceito, esta opção serve.

Note o workflowId nos argumentos: é esse parâmetro que vincula a chamada da ferramenta ao contexto certo. A parte cliente (widget ou agente) precisa armazená-lo em algum lugar e repassá-lo.

6. Integração com o Apps SDK: de onde obter workflowId e sessionId

A pergunta “de onde vem o workflowId” em ChatGPT Apps é um pouco filosófica. As possibilidades dependem de você usar autenticação, MCP diretamente ou o Agents SDK. Em linhas gerais, as opções são: gerar no lado do servidor na primeira chamada de ferramenta ou gerar no widget e enviar para baixo.

Para o exemplo didático, vamos supor que a primeira etapa é a chamada de uma ferramenta MCP que cria o workflow, e o widget depois apenas consome seu id.

A opção mais simples:

// mcp/tools/startWorkflow.ts
import { randomUUID } from "crypto";
import { saveWorkflow } from "../workflowStore";

export const startWorkflowTool = {
  name: "gift_start_workflow",
  description: "Cria um novo workflow de seleção de presentes",
  inputSchema: { type: "object", properties: {} },
  async run() {
    const id = randomUUID();
    saveWorkflow({
      id,
      currentStep: "profile",
      likedIdeaIds: [],
      hiddenIdeaIds: [],
      updatedAt: Date.now()
    });
    return {
      structuredContent: {
        type: "workflowStarted",
        workflowId: id,
        currentStep: "profile"
      }
    };
  }
};

Depois disso, o modelo, ao receber o workflowId na resposta da ferramenta, pode:

  • registrá-lo de forma oculta no contexto;
  • passá-lo ao widget por meio de structuredContent, para que o widget salve esse valor no widgetState e passe a incluí-lo nas próximas chamadas de ferramentas.

No lado do widget, o código será mais ou menos assim.

7. Armazenando o workflowId e o estado de UI local no widget

Suponha que temos um widget de lista de ideias, que precisa saber qual workflow ele exibe e lembrar as curtidas locais, mesmo se o componente for desmontado. Em uma versão simplificada:

// app/widgets/GiftIdeasWidget.tsx
import { useEffect, useState } from "react";

interface Idea {
  id: string;
  title: string;
}

interface WidgetProps {
  widgetId: string;
  workflowId: string;   // veio de structuredContent
  ideas: Idea[];
}

interface UiState {
  liked: string[];
}

export function GiftIdeasWidget(props: WidgetProps) {
  const [uiState, setUiState] = useState<UiState>({ liked: [] });

  useEffect(() => {
    window.openai.getWidgetState<UiState>(props.widgetId).then(saved => {
      if (saved) setUiState(saved);
    });
  }, [props.widgetId]);

  function toggleLike(id: string) {
    const exists = uiState.liked.includes(id);
    const next: UiState = {
      liked: exists
        ? uiState.liked.filter(x => x !== id)
        : [...uiState.liked, id]
    };
    setUiState(next);
    window.openai.setWidgetState(props.widgetId, next);
    // aqui também é possível chamar a MCP-tool "gift_like_idea"
  }

  return (
    <ul>
      {props.ideas.map(idea => (
        <li key={idea.id}>
          {idea.title}
          <button onClick={() => toggleLike(idea.id)}>
            {uiState.liked.includes(idea.id) ? "★" : "☆"}
          </button>
        </li>
      ))}
    </ul>
  );
}

Aqui o widgetState é usado exatamente como uma camada de UI: lembramos quais ideias estão destacadas. Idealmente, as curtidas também devem ser enviadas ao servidor (por uma ferramenta MCP ou um endpoint de API no Next.js), para que a camada de negócio também saiba o que o usuário escolheu.

É importante não tentar construir todo o workflow em cima do widgetState. Ele deve ser uma camada adicional ao contexto de negócio no servidor.

8. Restauração do roteiro: o usuário voltou

Agora vamos a um caso mais interessante: o usuário fechou o ChatGPT, voltou depois de algumas horas ou dias e abriu novamente o mesmo chat. O que deve acontecer?

O UX ideal é: o modelo e o App entendem que o usuário já tem um workflow não finalizado, trazem o contexto dele e dizem algo como: “Você já informou o perfil e o orçamento, vamos continuar com a escolha das ideias”.

Arquiteturalmente, isso fica assim:

  1. No seu servidor está armazenado o GiftWorkflowContext, vinculado a algum userId ou pelo menos a um workflowId interno.
  2. Em uma nova requisição (ou na primeira chamada de ferramenta no contexto do diálogo), o App consulta o servidor perguntando: “Existe um workflow ativo para este usuário?”.
  3. Se existir, o servidor o devolve e, talvez, uma flag especial resume, que o modelo usa em sua réplica.

Em um demo monolítico simples, você pode considerar que o servidor MCP e o aplicativo Next.js vivem no mesmo repositório (ou até no mesmo processo), então simplesmente reutilizamos o mesmo workflowStore do MCP também nas rotas de API.

No Next.js isso pode ser uma rota de API simples:

// app/api/gift/workflow/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkflow } from "@/mcp/workflowStore"; // neste demo, MCP e Next.js compartilham o mesmo armazenamento

export async function GET(req: NextRequest) {
  const id = req.nextUrl.searchParams.get("workflowId");
  if (!id) return NextResponse.json({ error: "Missing workflowId" }, { status: 400 });

  const ctx = getWorkflow(id);
  if (!ctx) return NextResponse.json({ exists: false });

  return NextResponse.json({
    exists: true,
    context: ctx
  });
}

O widget (ou a ferramenta MCP) pode chamar esse endpoint quando precisar atualizar o estado: por exemplo, na primeira montagem ou ao alternar a etapa. Na configuração didática, basta a combinação workflowId + storage em Map; em produção real, você adicionará autenticação e verificação de propriedade do usuário.

Se você usa o Agents SDK ou uma orquestração mais complexa, pode ampliar a ideia para “checkpoints” — salvar o estado ao final de grandes etapas, de onde o agente pode continuar ao reiniciar. Mas isso já é tema do próximo módulo.

9. Avançar e voltar e histórico de etapas

Inevitavelmente surge a pergunta: “É possível voltar uma etapa?”. Para o usuário isso é muito natural: alterar o orçamento, ajustar interesses, remover um item da seleção.

Tecnicamente, isso significa duas coisas:

  • é preciso armazenar não só a etapa atual, mas também o histórico de decisões tomadas;
  • é preciso recalcular com cuidado os dados derivados após o rollback.

Uma das opções é adicionar ao contexto o campo history, que conterá snapshots das etapas. Por exemplo:

export interface StepSnapshot {
  step: GiftWorkflowStep;
  payload: any;          // dados específicos da etapa
  createdAt: number;
}

export interface GiftWorkflowContext {
  // ...campos anteriores
  history: StepSnapshot[];
}

Quando o usuário preenche o perfil, você adiciona ao histórico um snapshot com step: "profile". Quando altera o orçamento — mais um snapshot. Ao fazer rollback para o perfil, você:

  • atualiza currentStep = "profile";
  • opcionalmente corta o histórico até o índice necessário;
  • recalcula os valores derivados (por exemplo, limpa ideias e curtidas, se dependerem do orçamento).

No nível do modelo, é importante sincronizar: se o usuário clicou no botão “Voltar” no widget, é preciso enviar uma chamada de ferramenta que atualize o contexto de negócio e retorne na resposta uma descrição explícita do novo estado. Caso contrário, você terá o problema clássico de dessincronização: a UI mostra a etapa 2, o modelo tem certeza de que você está na etapa 3.

No nível do widget, o rollback pode parecer um botão simples:

async function goBackToProfile() {
  await fetch("/api/gift/workflow/back", {
    method: "POST",
    body: JSON.stringify({ workflowId, targetStep: "profile" })
  });
  // atualizamos a UI e limpamos o estado local
}

E então o servidor decide exatamente o que limpar no contexto e que mensagem enviar ao modelo por meio da resposta da ferramenta.

10. Como conectar tudo isso ao modelo: contexto para o raciocínio

Tudo o que fazemos com o estado, no fim, é necessário não apenas para o usuário, mas também para o LLM. O modelo deve entender:

  • o que já se sabe (por exemplo, o perfil do destinatário e o orçamento);
  • quais etapas já foram concluídas;
  • se há processos não finalizados.

A forma de entregar essa informação ao modelo depende da arquitetura do App: você pode injetá-la no system prompt, devolvê-la no ToolOutput de forma estruturada ou usar campos especiais _meta/annotations, se o SDK os suportar.

Um padrão típico é:

  1. A ferramenta MCP retorna no structuredContent um snapshot curto do contexto: etapa atual, campos-chave e, possivelmente, o workflowId.
  2. O Apps SDK transforma isso em um widget ou em texto + dados ocultos.
  3. O modelo, ao ver o structuredContent, entende que o roteiro foi retomado e constrói a próxima ação a partir disso.

Em alguns casos, se o modelo “esqueceu” parâmetros importantes ou começou a alucinar, você pode atualizar o contexto à força: chamar uma ferramenta especial que devolva o estado atual e o modelo “entra novamente no contexto”.

É importante não tentar enfiar no modelo todo o GiftWorkflowContext até o último campo. Bastam os pontos-chave: para quem estamos buscando o presente, qual é o orçamento, quantas ideias já foram exibidas e se há um checkout não finalizado.

11. Mini checklist ao projetar o WorkflowContext

Antes de passar aos erros típicos, é útil formular um pequeno conjunto de perguntas às quais você deve responder ao projetar o contexto do workflow (você pode literalmente anotar isso ao lado da interface):

  • Quais etapas o roteiro tem e qual é o conjunto mínimo de dados necessário em cada uma?
    Isso o protegerá de JSONs monstruosos “por via das dúvidas”.
  • O que precisa ser lembrado apenas dentro de um chat e o que precisa ser lembrado entre sessões e dispositivos?
    O primeiro pode ficar no widgetState e nos prompts; o segundo deve obrigatoriamente ir para o banco de dados do servidor.
  • Como será o identificador do contexto?
    Pode ser a combinação userId + scenario, um workflowId separado ou ambos. O principal é que você consiga encontrar o contexto de forma inequívoca no banco.
  • Como você vai limpar workflows antigos?
    Para demos, “nunca limpar” pode ser aceitável, mas em produção você vai precisar de TTL ou de jobs em background que removam workflows antigos.
  • O usuário precisa voltar atrás e como você vai implementar isso?
    Você vai armazenar uma árvore de ramificações ou basta uma lista linear de etapas com possibilidade de rollback.

E por fim: tente simular mentalmente o cenário “o usuário voltou depois de uma semana em outro chat”. Se você não consegue explicar como o App descobrirá o workflow antigo e o que deve mostrar, é preciso reforçar a parte de armazenamento persistente.

12. Erros típicos ao trabalhar com o contexto entre etapas

Erro nº 1: armazenar tudo apenas no histórico do diálogo.
Às vezes surge a tentação: “Bem, o modelo vê tudo no texto, vamos simplesmente listar no prompt qual é o orçamento, quais produtos e o que o usuário escolheu”. Essa abordagem rapidamente bate nos limites de contexto e não oferece nenhuma garantia de integridade: o modelo pode “esquecer” um fato importante ou confundir identificadores. Coisas críticas de negócio (dinheiro, reservas, pedidos) devem viver no seu backend/MCP como fonte da verdade.

Erro nº 2: tentar construir todo o workflow apenas com widgetState.
O widgetState no Apps SDK resolve o problema de sobrevivência do estado de UI entre desmontagens e remontagens do widget, e não o de armazenamento de longo prazo do workflow. Se tentar armazenar nele o perfil, o carrinho e o histórico de etapas, você terá caos ao trocar de dispositivo e a impossibilidade de se restaurar após um longo tempo. O widget responde por detalhes visuais e conforto local. Toda a lógica do roteiro deve viver no servidor.

Erro nº 3: ausência de um workflowId explícito ou outra chave.
Acontece de o desenvolvedor confiar em identificadores implícitos como conversation_id, mas não introduzir seu próprio conceito de workflow. O resultado é que fica impossível distinguir um roteiro de outro, separar vários workflows paralelos ou restaurar exatamente o que é necessário. Uma simples string workflowId em todos os lugares onde há ferramentas e endpoints de API resolve muitos problemas, especialmente no MCP, que é stateless por protocolo.

Erro nº 4: misturar estado de UI e lógica de negócio.
Situação clássica: no widgetState colocam não só “qual aba está aberta”, mas também “quais produtos estão no carrinho”, e depois tentam tomar decisões no servidor com base nesse estado. No fim, com a menor dessincronização (o widget renderizou, mas a requisição ainda não chegou, ou vice-versa), o modelo vê uma realidade, a UI outra e o banco uma terceira. A fronteira de responsabilidade deve ser clara: o servidor armazena e valida os dados de negócio, o widget os exibe e dá ao usuário uma forma conveniente de alterá-los.

Erro nº 5: ausência de um roteiro de restauração e rollback.
É muito fácil desenhar um “caminho feliz” bonito, no qual o usuário segue perfeitamente as etapas, nada quebra, o ChatGPT não recarrega e a conexão não cai. Na realidade, cada etapa pode falhar, o usuário pode sair no meio e voltar uma semana depois. Se você não modelou a estrutura do WorkflowContext, não pensou em como buscar o workflow “ativo” e não previu os botões “Voltar” e “Continuar depois”, seu roteiro será frágil e frustrante para os usuários. Um contexto bem projetado é a base para a tolerância a falhas, tema da próxima aula.

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