Ir al contenido

Pipeline de generación de `/handoff`

Este documento describe cómo el agente de codificación implementa /handoff actualmente: ruta de activación, prompt de generación, captura de finalización, cambio de sesión y reinyección de contexto.

Cubre:

  • Despacho interactivo del comando /handoff
  • Ciclo de vida y transiciones de estado de AgentSession.handoff()
  • Cómo se captura la salida del handoff desde la salida del asistente
  • Cómo las sesiones antiguas/nuevas persisten los datos de handoff de forma diferente
  • Comportamiento de la interfaz de usuario para éxito, cancelación y fallo

No cubre:

  • Navegación genérica de árbol e internos de ramificación
  • Comandos de sesión que no son de handoff (/new, /fork, /resume)
  1. /handoff se declara en los metadatos de comandos slash integrados (slash-commands.ts) con una sugerencia en línea opcional: [focus instructions].
  2. En el manejo interactivo de entrada (InputController), el texto enviado que coincide con /handoff o /handoff ... es interceptado antes del envío normal del prompt.
  3. El editor se limpia y se llama a handleHandoffCommand(customInstructions?).
  4. CommandController.handleHandoffCommand realiza una verificación previa usando las entradas actuales:
    • Cuenta las entradas con type === "message".
    • Si < 2, advierte: Nothing to hand off (no messages yet) y retorna.

La misma guarda de contenido mínimo existe nuevamente dentro de AgentSession.handoff() y lanza un error si se viola. Esto duplica la seguridad tanto en la capa de interfaz de usuario como en la de sesión.

AgentSession.handoff(customInstructions?):

  • Lee las entradas de la rama actual (sessionManager.getBranch())
  • Valida el conteo mínimo de mensajes (>= 2)
  • Crea #handoffAbortController
  • Construye un prompt fijo en línea que solicita un documento de handoff estructurado (Goal, Constraints & Preferences, Progress, Key Decisions, Critical Context, Next Steps)
  • Agrega Additional focus: ... si se proporcionan instrucciones personalizadas

El prompt se envía mediante:

await this.prompt(handoffPrompt, { expandPromptTemplates: false });

expandPromptTemplates: false evita la expansión de slash/plantillas de prompt de esta carga de instrucciones interna.

Antes de enviar el prompt, handoff() se suscribe a los eventos de sesión y espera agent_end.

Al recibir agent_end, extrae el texto del handoff del estado del agente buscando hacia atrás el mensaje assistant más reciente, luego concatena todos los bloques content donde type === "text" con \n.

Supuestos importantes de extracción:

  • Solo se usan bloques de texto; el contenido que no sea texto se ignora.
  • Se asume que el último mensaje del asistente corresponde a la generación del handoff.
  • No analiza secciones markdown ni valida el cumplimiento del formato.
  • Si la salida del asistente no tiene bloques de texto, el handoff se trata como ausente.

handoff() retorna undefined cuando se cumple alguna de las siguientes condiciones:

  • no hay texto de handoff capturado, o
  • #handoffAbortController.signal.aborted es verdadero

Siempre limpia #handoffAbortController en finally.

Si se capturó texto y no fue abortado:

  1. Vaciar el escritor de la sesión actual (sessionManager.flush())
  2. Iniciar una sesión completamente nueva (sessionManager.newSession())
  3. Reiniciar el estado del agente en memoria (agent.reset())
  4. Reasignar agent.sessionId al id de la nueva sesión
  5. Limpiar los arreglos de contexto en cola (#steeringMessages, #followUpMessages, #pendingNextTurnMessages)
  6. Reiniciar el contador de recordatorio de tareas pendientes

newSession() crea un encabezado nuevo y una lista de entradas vacía (la hoja se reinicia a null). En la ruta de handoff, no se pasa ningún parentSession.

El documento de handoff generado se envuelve y se agrega a la nueva sesión como una entrada custom_message:

<handoff-context>
...handoff text...
</handoff-context>
The above is a handoff document from a previous session. Use this context to continue the work seamlessly.

Llamada de inserción:

this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);

Semántica:

  • customType: "handoff"
  • display: true (visible en la reconstrucción de la TUI)
  • Tipo de entrada: custom_message (participa en el contexto del LLM)

6) Reconstrucción del contexto activo del agente

Sección titulada «6) Reconstrucción del contexto activo del agente»

Después de la inyección:

  1. sessionManager.buildSessionContext() resuelve la lista de mensajes para la hoja actual
  2. agent.replaceMessages(sessionContext.messages) hace que el mensaje de handoff inyectado sea el contexto activo
  3. El método retorna { document: handoffText }

En este punto, el contexto activo del LLM en la nueva sesión contiene el mensaje de handoff inyectado, no la transcripción anterior.

Modelo de persistencia: sesión antigua vs sesión nueva

Sección titulada «Modelo de persistencia: sesión antigua vs sesión nueva»

Durante la generación, la persistencia normal de mensajes permanece activa. La respuesta de handoff del asistente se persiste como una entrada regular message al ocurrir message_end.

Resultado: la sesión original contiene el handoff generado y visible como parte de la transcripción histórica.

Después del reinicio de sesión, el handoff se persiste como custom_message con customType: "handoff".

buildSessionContext() convierte esta entrada en un mensaje de contexto personalizado/de usuario en tiempo de ejecución mediante createCustomMessage(...), de modo que se incluye en los futuros prompts de la nueva sesión.

Comportamiento del controlador/interfaz de usuario

Sección titulada «Comportamiento del controlador/interfaz de usuario»

Comportamiento de CommandController.handleHandoffCommand:

  • Llama a await session.handoff(customInstructions)
  • Si el resultado es undefined: showError("Handoff cancelled")
  • En caso de éxito:
    • rebuildChatFromMessages() (carga el nuevo contexto de sesión, incluido el handoff inyectado)
    • invalida la línea de estado y el borde superior del editor
    • recarga las tareas pendientes
    • agrega una línea de chat de éxito: New session started with handoff context
  • En caso de excepción:
    • si el mensaje es "Handoff cancelled" o el nombre del error es AbortError: showError("Handoff cancelled")
    • de lo contrario: showError("Handoff failed: <message>")
  • Solicita renderizado al final

Semántica de cancelación (comportamiento actual)

Sección titulada «Semántica de cancelación (comportamiento actual)»

Primitiva de cancelación a nivel de sesión

Sección titulada «Primitiva de cancelación a nivel de sesión»

AgentSession expone:

  • abortHandoff() → aborta #handoffAbortController
  • isGeneratingHandoff → verdadero mientras existe el controlador

Cuando se usa esta ruta de aborto, el suscriptor del handoff rechaza con Error("Handoff cancelled"), y el controlador de comandos lo mapea a la interfaz de usuario de cancelación.

Limitación de la ruta interactiva de /handoff

Sección titulada «Limitación de la ruta interactiva de /handoff»

En el cableado actual del controlador interactivo, /handoff no instala un manejador dedicado de Escape que llame a abortHandoff() (a diferencia de las rutas de compactación/resumen de rama que anulan temporalmente editor.onEscape).

Impacto práctico:

  • Existe soporte de cancelación a nivel de sesión, pero no hay un enlace de tecla específico para handoff en la ruta del comando /handoff.
  • La interrupción del usuario aún puede ocurrir mediante rutas de aborto de agente más amplias, pero eso no es el mismo canal de cancelación explícito usado por abortHandoff().

Clasificación actual de la interfaz de usuario:

  • Abortado/cancelado

    • La ruta abortHandoff() activa "Handoff cancelled", o
    • se lanza AbortError
    • La interfaz muestra Handoff cancelled
  • Fallido

    • cualquier otro error lanzado desde handoff() / la canalización de prompts (errores de validación de modelo/API, excepciones en tiempo de ejecución, etc.)
    • La interfaz muestra Handoff failed: ...

Matiz adicional: si la generación se completa pero no se extrae texto, handoff() retorna undefined y el controlador actualmente reporta cancelado, no fallido.

Salvaguardas de sesión corta y contenido mínimo

Sección titulada «Salvaguardas de sesión corta y contenido mínimo»

Dos guardas evitan handoffs con poca información:

  • Capa de interfaz de usuario (handleHandoffCommand): advierte y retorna anticipadamente para < 2 entradas de mensajes
  • Capa de sesión (handoff()): lanza la misma condición como error

Esto evita crear una nueva sesión con un contexto de handoff vacío o casi vacío.

Flujo de estado de alto nivel:

  1. Comando slash interactivo interceptado
  2. Guarda de conteo de mensajes previa
  3. #handoffAbortController creado (isGeneratingHandoff = true)
  4. Prompt de handoff interno enviado (visible en el chat como generación normal del asistente)
  5. Al recibir agent_end, se extrae el último texto del asistente
  6. Si falta/fue abortado → retornar undefined o ruta de error de cancelación
  7. Si está presente:
    • vaciar la sesión antigua
    • crear una nueva sesión vacía
    • reiniciar colas/contadores en tiempo de ejecución
    • agregar custom_message(handoff)
    • reconstruir y reemplazar los mensajes activos del agente
  8. El controlador reconstruye la interfaz de chat y anuncia el éxito
  9. #handoffAbortController limpiado (isGeneratingHandoff = false)
  • La extracción del handoff es heurística: “últimos bloques de texto del asistente”; sin validación estructural.
  • No hay verificación estricta de que el markdown generado siga el formato de sección solicitado.
  • El texto extraído faltante se reporta como cancelación en la experiencia de usuario del controlador.
  • El flujo interactivo de /handoff actualmente carece de un enlace dedicado Escape→abortHandoff().
  • Los metadatos de linaje de la nueva sesión (parentSession) no se establecen mediante esta ruta.