CommunityProgramación y desarrollogithub.com

EBIKEBUILDER/ai-skills

Usa esta habilidad al construir o mejorar asistentes de chat, asistentes de codificación o editores de documentos/espacios de trabajo con Google Gemini (@google/genai) para utilizar conversaciones multi-turno con estado en lugar de reenviar bases de código, documentos o contexto completos en cada prompt.

¿Qué es ai-skills?

Esta habilidad resuelve un problema crítico en asistentes de IA: tratar cada interacción como una solicitud sin estado, reenviando documentos completos o bases de código en cada turno, lo que desperdicia tokens, aumenta la latencia y deteriora la calidad del razonamiento. En su lugar, propone dos patrones claros para mantener conversaciones multi-turno con estado usando la API de Gemini. El primer patrón utiliza la API de Interacciones gestionada por el servidor (@google/genai >= 2.3.0), que permite encadenar turnos mediante `previous_interaction_id` sin reenviar el contexto inicial. El segundo patrón es una estrategia del lado del cliente que construye un arreglo de contenidos con roles `user/model`, y sincroniza el espacio de trabajo mediante hash SHA-256 para omitir archivos no modificados en turnos posteriores. La habilidad incluye reglas detalladas para compactar turnos antiguos, eliminar código redundante y detectar cambios manuales del usuario. Es útil para desarrolladores que construyen asistentes de codificación, editores colaborativos o cualquier sistema que requiera múltiples interacciones con Gemini sobre un mismo contexto, y es compatible con TypeScript y Python.

Compatible con~Claude Code~Codex CLI~Cursor✓Gemini CLI
npx skills add https://github.com/EBIKEBUILDER/ai-skills/tree/HEAD/gemini-multiturn-conversation

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Conversaciones Multi-Turno con Estado en Gemini (@google/genai)

Problema Principal que Resuelve Esta Habilidad

Muchos asistentes de IA tratan accidentalmente cada prompt como una solicitud sin estado de un solo turno—re-subiendo el documento completo, la base de código o el estado del espacio de trabajo en cada pregunta de seguimiento o edición secuencial, o aplanando los turnos anteriores en un solo bloque de texto. Esto desperdicia tokens, aumenta la latencia y degrada la calidad del razonamiento.

En su lugar, mantén una conversación multi-turno con estado donde:

  1. El contexto inicial (código, documento o estado del proyecto) se establece en el Turno 1.
  2. Los turnos subsiguientes continúan la misma conversación (previous_interaction_id o turnos estructurados user/model en contents) sin reenviar el estado sin cambios.
  3. El código o documento completo solo se reenvía cuando el usuario edita manualmente el espacio de trabajo fuera del flujo de la IA (detectado mediante comparación de hash SHA-256).

Patrón 1: Estado Multi-Turno Gestionado por el Servidor (API de Interacciones)

Al usar la API de Interacciones de Google (@google/genai >= 2.3.0), Gemini mantiene el contexto de la conversación en los servidores de Google (store: true por defecto). Encadena los turnos pasando previous_interaction_id: interaction.id:

TypeScript (@google/genai)

import { GoogleGenAI } from "@google/genai";

const client = new GoogleGenAI({});

// Turno 1: Establece la conversación y el estado inicial del espacio de trabajo/documento
const turn1 = await client.interactions.create({
  model: "gemini-3.7-flash",
  system_instruction: "Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
  input: `Aquí está mi archivo actual:\n\`\`\`ts\n${initialCode}\n\`\`\`\nAñade validación de entrada.`,
});

console.log(turn1.output_text);

// Turno 2: Pregunta de seguimiento o edición secuencial — ¡NO reenvíes el archivo si no ha cambiado!
// El servidor de Google ya mantiene la entrada del Turno 1 y la salida del modelo a través de previous_interaction_id.
const turn2 = await client.interactions.create({
  model: "gemini-3.7-flash",
  system_instruction: "Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
  previous_interaction_id: turn1.id,
  input: "¿Por qué elegiste ese patrón de validación, y podemos también registrar las entradas rechazadas?",
});

console.log(turn2.output_text);

Python (google-genai)

from google import genai

client = genai.Client()

# Turno 1: Establece el contexto inicial
turn1 = client.interactions.create(
    model="gemini-3.7-flash",
    system_instruction="Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
    input=f"Aquí está mi archivo actual:\n```py\n{initial_code}\n```\nRefactoriza esto en una clase.",
)

# Turno 2: Encadena con previous_interaction_id sin re-subir el archivo
turn2 = client.interactions.create(
    model="gemini-3.7-flash",
    system_instruction="Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
    previous_interaction_id=turn1.id,
    input="Ahora añade un método de prueba unitaria para casos extremos.",
)

Reglas importantes para interactions.create:

  • system_instruction, tools y generation_config tienen alcance de interacción—vuelve a especificarlos en cada turno.
  • Mantén store: true (el valor predeterminado); establecer store: false deshabilita previous_interaction_id.

Patrón 2: contents Multi-Turno Gestionados por la Aplicación con Estado de Espacio de Trabajo Sincronizado por Hash

Cuando tu backend almacena los turnos de la conversación en su propia base de datos (o usa ai.models.generateContent / ai.chats.create con esquemas JSON estructurados y flujos de diff/aplicar), sigue estas cinco reglas para no reenviar nunca de manera redundante archivos o secciones del espacio de trabajo sin cambios:

1. Usa Arreglos de Turnos Nativos con role: 'user' | 'model'

Nunca aplanes turnos anteriores en una sola cadena dentro de un mensaje de usuario. Siempre construye un arreglo contents multi-turno adecuado:

type ContentTurn = {
  role: "user" | "model";
  parts: Array<{ text: string }>;
};

2. Rastrea la Sincronización del Espacio de Trabajo mediante SHA-256

Mantén un registro de sincronización por conversación en el cliente:

interface SyncedConversationState {
  activeDocumentSha256: string | null;
  secondaryContextHash: string | null; // p. ej., otras pestañas abiertas, escenas o resumen del árbol del proyecto
}
  • Cuando un turno de ask (P&R) se completa: activeDocumentSha256 permanece como el SHA-256 del documento que se envió.
  • Cuando un turno de edit se completa con una propuesta: activeDocumentSha256 se actualiza a proposal.sha256 (porque el turno model en el historial de la conversación ahora contiene el código propuesto).
  • Cuando el usuario hace clic en Aplicar: activeDocumentSha256 coincide con el proposal.sha256 aplicado.
  • Cuando el usuario hace clic en Descartar o Deshacer: activeDocumentSha256 revierte al base.sha256 anterior a la propuesta.

3. Omite el Código del Espacio de Trabajo sin Cambios en Turnos Posteriores (sourceSynchronized: true)

Antes de enviar un nuevo turno, compara sha256(currentEditorCode) con syncedState.activeDocumentSha256:

  • Si sha256(currentEditorCode) === syncedState.activeDocumentSha256 (sourceSynchronized: true) :
    • En modo Ask (P&R): No subas el archivo en absoluto (source: null). El modelo ya tiene el código activo exacto en su historial de conversación.
    • En modo Edit: No dupliques currentSource dentro del nuevo turno user enviado a Gemini ({ mode: "edit", prompt, sourceSynchronized: true }). Mantén la instantánea base local solo para el diff/Undo del lado del cliente y la validación del backend.
  • Si sha256(currentEditorCode) !== syncedState.activeDocumentSha256 (sourceSynchronized: false) :
    • El usuario editó manualmente el archivo o cambió de archivo. Adjunta el currentSource actualizado en este turno para resincronizar la conversación y actualiza activeDocumentSha256.

4. Mantén la Fuente Completa Solo una Vez en el Historial (Compactar Turnos Reemplazados)

En una sesión de edición de 10 turnos, no mantengas 10 copias completas del archivo en los 10 turnos históricos.

  • Mantén el código completo solo en el turno más reciente que introdujo o modificó el código activo (ya sea el turno model que propuso la última edición activa, o el turno user que adjuntó por última vez código editado manualmente).
  • Para todos los turnos anteriores cuyo código fue reemplazado, reemplaza la cadena de código en línea con metadatos compactos ({ supersededSourceSha256: "..." }). Esto mantiene el uso de tokens plano en sesiones largas mientras preserva el hilo completo de razonamiento conversacional.

5. Desduplica el Contexto Secundario de Solo Lectura (Otros Archivos / Pestañas / Secciones)

Si tu aplicación incluye un resumen de solo lectura de archivos o secciones no activos:

  • Envía ese resumen en el Turno 1 de la conversación.
  • En el Turno 2+, solo reenvía el resumen si su hash cambió (secondaryContextHash !== currentSecondaryContextHash).

Implementación de Referencia: Constructor de Solicitudes Multi-Turno (TypeScript)

interface HistoryTurn {
  prompt: string;
  response: string;
  mode: "ask" | "edit";
  baseSource?: string | null;
  baseSha256?: string | null;
  proposalSource?: string | null;
  proposalSha256?: string | null;
  outcomeStatus?: "none" | "proposed" | "applied" | "discarded" | "undone";
}

export function buildMultiTurnContents(params: {
  history: HistoryTurn[];
  mode: "ask" | "edit";
  prompt: string;
  currentSource: string | null;
  sourceSynchronized: boolean;
}) {
  const { history, mode, prompt, currentSource, sourceSynchronized } = params;

  // Identifica qué turno histórico contiene la fuente actualmente activa
  let latestActiveSourceIndex = -1;
  if (sourceSynchronized || currentSource === null) {
    for (let i = history.length - 1; i >= 0; i--) {
      const item = history[i];
      const hasValidProposal =
        item.mode === "edit" &&
        item.proposalSource &&
        item.outcomeStatus !== "discarded" &&
        item.outcomeStatus !== "undone";
      if (hasValidProposal || item.baseSource) {
        latestActiveSourceIndex = i;
        break;
      }
    }
  }

  const contents: Array<{ role: "user" | "model"; parts: Array<{ text: string }> }> = [];

  for (let i = 0; i < history.length; i++) {
    const item = history[i];
    const isLatestSourceTurn = i === latestActiveSourceIndex;
    const hasActiveProposal =
      item.mode === "edit" &&
      Boolean(item.proposalSource) &&
      item.outcomeStatus !== "discarded" &&
      item.outcomeStatus !== "undone";

    // Turno de usuario en el historial
    const userPayload: Record<string, unknown> = {
      mode: item.mode ?? "ask",
      prompt: item.prompt,
    };
    if (isLatestSourceTurn && !hasActiveProposal && item.baseSource) {
      userPayload.currentSource = item.baseSource;
    } else if (item.baseSha256) {
      userPayload.supersededSourceSha256 = item.baseSha256;
    }
    contents.push({ role: "user", parts: [{ text: JSON.stringify(userPayload) }] });

    // Turno del modelo en el historial
    const modelPayload: Record<string, unknown> = {
      kind: item.mode === "edit" ? "code_proposal" : "answer",
      explanation: item.response,
    };
    if (isLatestSourceTurn && hasActiveProposal && item.proposalSource) {
      modelPayload.proposedSource = item.proposalSource;
    } else if (item.proposalSha256) {
      modelPayload.supersededProposalSha256 = item.proposalSha256;
    }
    contents.push({ role: "model", parts: [{ text: JSON.stringify(modelPayload) }] });
  }

  // Turno final (actual) del usuario
  const omitDuplicateSource = sourceSynchronized && latestActiveSourceIndex !== -1;
  const currentTurnPayload: Record<string, unknown> = { mode, prompt };
  if (omitDuplicateSource) {
    currentTurnPayload.sourceSynchronized = true;
  } else {
    currentTurnPayload.currentSource = currentSource;
  }

  contents.push({ role: "user", parts: [{ text: JSON.stringify(currentTurnPayload) }] });
  return contents;
}

Lista de Verificación al Revisar o Implementar IA Multi-Turno

  • ¿Estamos usando @google/genai (GoogleGenAI) y nunca el obsoleto @google/generative-ai?
  • ¿El Turno 2+ preserva los roles multi-turno user / model (o previous_interaction_id) en lugar de aplanar el historial en una sola cadena de prompt?
  • En preguntas de seguimiento (modo Ask) cuando el código del editor no ha cambiado, ¿omitimos re-subir el código desde el cliente?
  • En ediciones secuenciales de IA (modo Edit) cuando el código del editor coincide con el último turno, ¿omitimos el currentSource duplicado del nuevo prompt de usuario en contents?
  • ¿Las versiones de código antiguas reemplazadas están compactadas en el historial para que solo la versión de código activa se envíe completa?
  • Si el usuario edita manualmente el documento o deshace un cambio, ¿la discrepancia de SHA-256 activa automáticamente la resincronización en el siguiente turno?

Skills relacionados