Saltearse al contenido

Modo RPC

El modo RPC permite operar el agente de codificación sin interfaz mediante un protocolo JSON por stdin/stdout. Es útil para integrar el agente en otras aplicaciones, IDEs o UIs personalizadas.

Nota para usuarios de Node.js/TypeScript: Si estás construyendo una aplicación Node.js, considera usar AgentSession directamente desde @earendil-works/pi-coding-agent en lugar de spawnear un subproceso. Consulta src/core/agent-session.ts para la API. Para un cliente TypeScript basado en subproceso, consulta src/modes/rpc/rpc-client.ts.

Ventana de terminal
pi --mode rpc [options]

Opciones comunes:

  • --provider <name>: Establecer el proveedor LLM (anthropic, openai, google, etc.)
  • --model <pattern>: Patrón o ID del modelo (soporta provider/id y opcional :<thinking>)
  • --name <name> / -n <name>: Establecer el nombre de visualización de la sesión al iniciar
  • --no-session: Deshabilitar persistencia de sesión
  • --session-dir <path>: Directorio personalizado de almacenamiento de sesiones
  • Commands (comandos): Objetos JSON enviados a stdin, uno por línea
  • Responses (respuestas): Objetos JSON con type: "response" que indican éxito/fallo del comando
  • Events (eventos): Eventos del agente transmitidos a stdout como líneas JSON

Todos los comandos soportan un campo opcional id para correlación solicitud/respuesta. Si se proporciona, la respuesta correspondiente incluirá el mismo id.

El modo RPC usa semántica JSONL estricta con LF (\n) como único delimitador de registro.

Esto importa para los clientes:

  • Dividir registros solo en \n
  • Aceptar entrada opcional \r\n eliminando un \r final
  • No usar lectores de línea genéricos que traten separadores Unicode como saltos de línea

En particular, Node readline no cumple el protocolo RPC porque también divide en U+2028 y U+2029, que son válidos dentro de cadenas JSON.

Enviar un prompt de usuario al agente. La respuesta del comando se emite después de que el prompt sea aceptado, encolado o manejado. Los eventos continúan transmitiéndose de forma asíncrona tras la aceptación.

{"id": "req-1", "type": "prompt", "message": "Hello, world!"}

Con imágenes:

{"type": "prompt", "message": "What's in this image?", "images": [{"type": "image", "data": "base64-encoded-data", "mimeType": "image/png"}]}

Durante streaming: Si el agente ya está transmitiendo, debes especificar streamingBehavior para encolar el mensaje:

{"type": "prompt", "message": "New instruction", "streamingBehavior": "steer"}
  • "steer": Encolar el mensaje mientras el agente está en ejecución. Se entrega después de que el turno actual del assistant termine de ejecutar sus llamadas a herramientas, antes de la siguiente llamada LLM.
  • "followUp": Esperar hasta que el agente termine. El mensaje se entrega solo cuando el agente se detiene.

Si el agente está transmitiendo y no se especifica streamingBehavior, el comando devuelve un error.

Comandos de extensión: Si el mensaje es un comando de extensión (p. ej., /mycommand), se ejecuta inmediatamente incluso durante streaming. Los comandos de extensión gestionan su propia interacción LLM vía pi.sendMessage().

Expansión de entrada: Los comandos skill (/skill:name) y plantillas de prompt (/template) se expanden antes de enviar/encolar.

Respuesta:

{"id": "req-1", "type": "response", "command": "prompt", "success": true}

success: true significa que el prompt fue aceptado, encolado o manejado inmediatamente. success: false significa que el prompt fue rechazado antes de la aceptación. Los fallos tras la aceptación se reportan a través del flujo normal de eventos y mensajes, no como un segundo response para el mismo id de solicitud.

El campo images es opcional. Cada imagen usa el formato ImageContent: {"type": "image", "data": "base64-encoded-data", "mimeType": "image/png"}.

Encolar un mensaje de steering mientras el agente está en ejecución. Se entrega después de que el turno actual del assistant termine de ejecutar sus llamadas a herramientas, antes de la siguiente llamada LLM. Los comandos skill y plantillas de prompt se expanden. No se permiten comandos de extensión (usa prompt en su lugar).

{"type": "steer", "message": "Stop and do this instead"}

Con imágenes:

{"type": "steer", "message": "Look at this instead", "images": [{"type": "image", "data": "base64-encoded-data", "mimeType": "image/png"}]}

El campo images es opcional. Cada imagen usa el formato ImageContent (igual que prompt).

Respuesta:

{"type": "response", "command": "steer", "success": true}

Consulta set_steering_mode para controlar cómo se procesan los mensajes de steering.

Encolar un mensaje follow-up para procesar después de que el agente termine. Se entrega solo cuando el agente no tiene más llamadas a herramientas ni mensajes de steering. Los comandos skill y plantillas de prompt se expanden. No se permiten comandos de extensión (usa prompt en su lugar).

{"type": "follow_up", "message": "After you're done, also do this"}

Con imágenes:

{"type": "follow_up", "message": "Also check this image", "images": [{"type": "image", "data": "base64-encoded-data", "mimeType": "image/png"}]}

El campo images es opcional. Cada imagen usa el formato ImageContent (igual que prompt).

Respuesta:

{"type": "response", "command": "follow_up", "success": true}

Consulta set_follow_up_mode para controlar cómo se procesan los mensajes follow-up.

Abortar la operación actual del agente.

{"type": "abort"}

Respuesta:

{"type": "response", "command": "abort", "success": true}

Iniciar una sesión nueva. Puede cancelarse mediante un manejador de eventos de extensión session_before_switch.

{"type": "new_session"}

Con seguimiento opcional de sesión padre:

{"type": "new_session", "parentSession": "/path/to/parent-session.jsonl"}

Respuesta:

{"type": "response", "command": "new_session", "success": true, "data": {"cancelled": false}}

Si una extensión canceló:

{"type": "response", "command": "new_session", "success": true, "data": {"cancelled": true}}

Obtener el estado actual de la sesión.

{"type": "get_state"}

Respuesta:

{
"type": "response",
"command": "get_state",
"success": true,
"data": {
"model": {...},
"thinkingLevel": "medium",
"isStreaming": false,
"isCompacting": false,
"steeringMode": "all",
"followUpMode": "one-at-a-time",
"sessionFile": "/path/to/session.jsonl",
"sessionId": "abc123",
"sessionName": "my-feature-work",
"autoCompactionEnabled": true,
"messageCount": 5,
"pendingMessageCount": 0
}
}

El campo model es un objeto Model completo o null. El campo sessionName es el nombre de visualización establecido vía set_session_name, u omitido si no está definido.

Obtener todos los mensajes de la conversación.

{"type": "get_messages"}

Respuesta:

{
"type": "response",
"command": "get_messages",
"success": true,
"data": {"messages": [...]}
}

Los mensajes son objetos AgentMessage (consulta Message Types).

Cambiar a un modelo específico.

{"type": "set_model", "provider": "anthropic", "modelId": "claude-sonnet-4-20250514"}

La respuesta contiene el objeto Model completo:

{
"type": "response",
"command": "set_model",
"success": true,
"data": {...}
}

Ciclar al siguiente modelo disponible. Devuelve datos null si solo hay un modelo disponible.

{"type": "cycle_model"}

Respuesta:

{
"type": "response",
"command": "cycle_model",
"success": true,
"data": {
"model": {...},
"thinkingLevel": "medium",
"isScoped": false
}
}

El campo model es un objeto Model completo.

Listar todos los modelos configurados.

{"type": "get_available_models"}

La respuesta contiene un array de objetos Model completos:

{
"type": "response",
"command": "get_available_models",
"success": true,
"data": {
"models": [...]
}
}

Establecer el nivel de razonamiento/pensamiento para modelos que lo soportan.

{"type": "set_thinking_level", "level": "high"}

Niveles: "off", "minimal", "low", "medium", "high", "xhigh"

Nota: "xhigh" solo está soportado por modelos OpenAI codex-max.

Respuesta:

{"type": "response", "command": "set_thinking_level", "success": true}

Ciclar por los niveles de pensamiento disponibles. Devuelve datos null si el modelo no soporta pensamiento.

{"type": "cycle_thinking_level"}

Respuesta:

{
"type": "response",
"command": "cycle_thinking_level",
"success": true,
"data": {"level": "high"}
}

Controlar cómo se entregan los mensajes de steering (desde steer).

{"type": "set_steering_mode", "mode": "one-at-a-time"}

Modos:

  • "all": Entregar todos los mensajes de steering después de que el turno actual del assistant termine de ejecutar sus llamadas a herramientas
  • "one-at-a-time": Entregar un mensaje de steering por turno completado del assistant (predeterminado)

Respuesta:

{"type": "response", "command": "set_steering_mode", "success": true}

Controlar cómo se entregan los mensajes follow-up (desde follow_up).

{"type": "set_follow_up_mode", "mode": "one-at-a-time"}

Modos:

  • "all": Entregar todos los mensajes follow-up cuando el agente termina
  • "one-at-a-time": Entregar un mensaje follow-up por finalización del agente (predeterminado)

Respuesta:

{"type": "response", "command": "set_follow_up_mode", "success": true}

Compactar manualmente el contexto de conversación para reducir el uso de tokens.

{"type": "compact"}

Con instrucciones personalizadas:

{"type": "compact", "customInstructions": "Focus on code changes"}

Respuesta:

{
"type": "response",
"command": "compact",
"success": true,
"data": {
"summary": "Summary of conversation...",
"firstKeptEntryId": "abc123",
"tokensBefore": 150000,
"estimatedTokensAfter": 32000,
"details": {}
}
}

estimatedTokensAfter es una estimación heurística sobre el contexto de mensajes reconstruido inmediatamente después de la compacción, no un conteo exacto de tokens del proveedor.

Habilitar o deshabilitar la compacción automática cuando el contexto está casi lleno.

{"type": "set_auto_compaction", "enabled": true}

Respuesta:

{"type": "response", "command": "set_auto_compaction", "success": true}

Habilitar o deshabilitar el reintento automático ante errores transitorios (sobrecarga, límite de tasa, 5xx).

{"type": "set_auto_retry", "enabled": true}

Respuesta:

{"type": "response", "command": "set_auto_retry", "success": true}

Abortar un reintento en curso (cancelar el retraso y dejar de reintentar).

{"type": "abort_retry"}

Respuesta:

{"type": "response", "command": "abort_retry", "success": true}

Ejecutar un comando shell y añadir la salida al contexto de conversación.

{"type": "bash", "command": "ls -la"}

Respuesta:

{
"type": "response",
"command": "bash",
"success": true,
"data": {
"output": "total 48\ndrwxr-xr-x ...",
"exitCode": 0,
"cancelled": false,
"truncated": false
}
}

Si la salida fue truncada, incluye fullOutputPath:

{
"type": "response",
"command": "bash",
"success": true,
"data": {
"output": "truncated output...",
"exitCode": 0,
"cancelled": false,
"truncated": true,
"fullOutputPath": "/tmp/pi-bash-abc123.log"
}
}

Cómo llegan los resultados de bash al LLM:

El comando bash se ejecuta inmediatamente y devuelve un BashResult. Internamente, se crea un BashExecutionMessage y se almacena en el estado de mensajes del agente. Este mensaje NO emite un evento.

Cuando se envía el siguiente comando prompt, todos los mensajes (incluido BashExecutionMessage) se transforman antes de enviarse al LLM. El BashExecutionMessage se convierte en un UserMessage con este formato:

Ran `ls -la`
```
total 48
drwxr-xr-x ...
```

Esto significa:

  1. La salida de bash se incluye en el contexto LLM en el siguiente prompt, no inmediatamente
  2. Se pueden ejecutar múltiples comandos bash antes de un prompt; todas las salidas se incluirán
  3. No se emite evento para el BashExecutionMessage en sí

Abortar un comando bash en ejecución.

{"type": "abort_bash"}

Respuesta:

{"type": "response", "command": "abort_bash", "success": true}

Obtener uso de tokens, estadísticas de coste y uso actual de la ventana de contexto.

{"type": "get_session_stats"}

Respuesta:

{
"type": "response",
"command": "get_session_stats",
"success": true,
"data": {
"sessionFile": "/path/to/session.jsonl",
"sessionId": "abc123",
"userMessages": 5,
"assistantMessages": 5,
"toolCalls": 12,
"toolResults": 12,
"totalMessages": 22,
"tokens": {
"input": 50000,
"output": 10000,
"cacheRead": 40000,
"cacheWrite": 5000,
"total": 105000
},
"cost": 0.45,
"contextUsage": {
"tokens": 60000,
"contextWindow": 200000,
"percent": 30
}
}
}

tokens contiene totales de uso del assistant para el estado actual de la sesión. contextUsage contiene la estimación actual real de la ventana de contexto usada para compacción y visualización en el pie de página.

contextUsage se omite cuando no hay modelo o ventana de contexto disponible. contextUsage.tokens y contextUsage.percent son null inmediatamente después de la compacción hasta que una respuesta fresca del assistant post-compacción proporcione datos de uso válidos.

Exportar la sesión a un archivo HTML.

{"type": "export_html"}

Con ruta personalizada:

{"type": "export_html", "outputPath": "/tmp/session.html"}

Respuesta:

{
"type": "response",
"command": "export_html",
"success": true,
"data": {"path": "/tmp/session.html"}
}

Cargar un archivo de sesión diferente. Puede cancelarse mediante un manejador de eventos de extensión session_before_switch.

{"type": "switch_session", "sessionPath": "/path/to/session.jsonl"}

Respuesta:

{"type": "response", "command": "switch_session", "success": true, "data": {"cancelled": false}}

Si una extensión canceló el cambio:

{"type": "response", "command": "switch_session", "success": true, "data": {"cancelled": true}}

Crear un nuevo fork desde un mensaje de usuario anterior en la rama activa. Puede cancelarse mediante un manejador de eventos de extensión session_before_fork. Devuelve el texto del mensaje desde el que se bifurca.

{"type": "fork", "entryId": "abc123"}

Respuesta:

{
"type": "response",
"command": "fork",
"success": true,
"data": {"text": "The original prompt text...", "cancelled": false}
}

Si una extensión canceló el fork:

{
"type": "response",
"command": "fork",
"success": true,
"data": {"text": "The original prompt text...", "cancelled": true}
}

Duplicar la rama activa actual en una nueva sesión en la posición actual. Puede cancelarse mediante un manejador de eventos de extensión session_before_fork.

{"type": "clone"}

Respuesta:

{
"type": "response",
"command": "clone",
"success": true,
"data": {"cancelled": false}
}

Si una extensión canceló el clone:

{
"type": "response",
"command": "clone",
"success": true,
"data": {"cancelled": true}
}

Obtener mensajes de usuario disponibles para fork.

{"type": "get_fork_messages"}

Respuesta:

{
"type": "response",
"command": "get_fork_messages",
"success": true,
"data": {
"messages": [
{"entryId": "abc123", "text": "First prompt..."},
{"entryId": "def456", "text": "Second prompt..."}
]
}
}

Obtener el contenido de texto del último mensaje del assistant.

{"type": "get_last_assistant_text"}

Respuesta:

{
"type": "response",
"command": "get_last_assistant_text",
"success": true,
"data": {"text": "The assistant's response..."}
}

Devuelve {"text": null} si no existen mensajes del assistant.

Establecer un nombre de visualización para la sesión actual. El nombre aparece en listados de sesiones y ayuda a identificarlas.

{"type": "set_session_name", "name": "my-feature-work"}

Respuesta:

{
"type": "response",
"command": "set_session_name",
"success": true
}

El nombre de sesión actual está disponible vía get_state en el campo sessionName. Para establecer el nombre inicial al iniciar el modo RPC, pasa --name <name> o -n <name> al proceso pi --mode rpc.

Obtener comandos disponibles (comandos de extensión, plantillas de prompt y skills). Se pueden invocar vía el comando prompt con prefijo /.

{"type": "get_commands"}

Respuesta:

{
"type": "response",
"command": "get_commands",
"success": true,
"data": {
"commands": [
{"name": "session-name", "description": "Set or clear session name", "source": "extension", "path": "/home/user/.pi/agent/extensions/session.ts"},
{"name": "fix-tests", "description": "Fix failing tests", "source": "prompt", "location": "project", "path": "/home/user/myproject/.pi/agent/prompts/fix-tests.md"},
{"name": "skill:brave-search", "description": "Web search via Brave API", "source": "skill", "location": "user", "path": "/home/user/.pi/agent/skills/brave-search/SKILL.md"}
]
}
}

Cada comando tiene:

  • name: Nombre del comando (invocar con /name)
  • description: Descripción legible (opcional para comandos de extensión)
  • source: Tipo de comando:
    • "extension": Registrado vía pi.registerCommand() en una extensión
    • "prompt": Cargado desde un archivo plantilla .md
    • "skill": Cargado desde un directorio skill (nombre con prefijo skill:)
  • location: Desde dónde se cargó (opcional, no presente para extensiones):
    • "user": Nivel usuario (~/.pi/agent/)
    • "project": Nivel proyecto (./.pi/agent/)
    • "path": Ruta explícita vía CLI o configuración
  • path: Ruta absoluta al origen del comando (opcional)

Nota: Los comandos TUI integrados (/settings, /hotkeys, etc.) no están incluidos. Solo se manejan en modo interactivo y no se ejecutarían si se envían vía prompt.

Los eventos se transmiten a stdout como líneas JSON durante la operación del agente. Los eventos NO incluyen un campo id (solo las respuestas).

EventDescription
agent_startEl agente comienza a procesar
agent_endEl agente completa (incluye todos los mensajes generados)
turn_startComienza un nuevo turno
turn_endEl turno completa (incluye mensaje del assistant y resultados de herramientas)
message_startComienza un mensaje
message_updateActualización en streaming (deltas de text/thinking/toolcall)
message_endCompleta un mensaje
tool_execution_startLa herramienta comienza ejecución
tool_execution_updateProgreso de ejecución de herramienta (salida en streaming)
tool_execution_endLa herramienta completa
queue_updateCambió la cola pendiente de steering/follow-up
compaction_startComienza la compacción
compaction_endCompleta la compacción
auto_retry_startComienza reintento automático (tras error transitorio)
auto_retry_endCompleta reintento automático (éxito o fallo final)
extension_errorLa extensión lanzó un error

Se emite cuando el agente comienza a procesar un prompt.

{"type": "agent_start"}

Se emite cuando el agente completa. Contiene todos los mensajes generados durante esta ejecución.

{
"type": "agent_end",
"messages": [...]
}

Un turno consiste en una respuesta del assistant más las llamadas a herramientas y resultados resultantes.

{"type": "turn_start"}
{
"type": "turn_end",
"message": {...},
"toolResults": [...]
}

Se emiten cuando un mensaje comienza y completa. El campo message contiene un AgentMessage.

{"type": "message_start", "message": {...}}
{"type": "message_end", "message": {...}}

Se emite durante el streaming de mensajes del assistant. Contiene tanto el mensaje parcial como un evento delta de streaming.

{
"type": "message_update",
"message": {...},
"assistantMessageEvent": {
"type": "text_delta",
"contentIndex": 0,
"delta": "Hello ",
"partial": {...}
}
}

El campo assistantMessageEvent contiene uno de estos tipos delta:

TypeDescription
startComenzó la generación del mensaje
text_startComenzó el bloque de contenido de texto
text_deltaFragmento de contenido de texto
text_endTerminó el bloque de contenido de texto
thinking_startComenzó el bloque de pensamiento
thinking_deltaFragmento de contenido de pensamiento
thinking_endTerminó el bloque de pensamiento
toolcall_startComenzó la llamada a herramienta
toolcall_deltaFragmento de argumentos de llamada a herramienta
toolcall_endTerminó la llamada a herramienta (incluye objeto toolCall completo)
doneMensaje completo (razón: "stop", "length", "toolUse")
errorOcurrió un error (razón: "aborted", "error")

Ejemplo de streaming de respuesta de texto:

{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_start","contentIndex":0,"partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello","partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":" world","partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Hello world","partial":{...}}}

tool_execution_start / tool_execution_update / tool_execution_end

Sección titulada «tool_execution_start / tool_execution_update / tool_execution_end»

Se emiten cuando una herramienta comienza, transmite progreso y completa la ejecución.

{
"type": "tool_execution_start",
"toolCallId": "call_abc123",
"toolName": "bash",
"args": {"command": "ls -la"}
}

Durante la ejecución, los eventos tool_execution_update transmiten resultados parciales (p. ej., salida bash a medida que llega):

{
"type": "tool_execution_update",
"toolCallId": "call_abc123",
"toolName": "bash",
"args": {"command": "ls -la"},
"partialResult": {
"content": [{"type": "text", "text": "partial output so far..."}],
"details": {"truncation": null, "fullOutputPath": null}
}
}

Al completar:

{
"type": "tool_execution_end",
"toolCallId": "call_abc123",
"toolName": "bash",
"result": {
"content": [{"type": "text", "text": "total 48\n..."}],
"details": {...}
},
"isError": false
}

Usa toolCallId para correlacionar eventos. El partialResult en tool_execution_update contiene la salida acumulada hasta el momento (no solo el delta), permitiendo a los clientes simplemente reemplazar su visualización en cada actualización.

Se emite siempre que cambia la cola pendiente de steering o follow-up.

{
"type": "queue_update",
"steering": ["Focus on error handling"],
"followUp": ["After that, summarize the result"]
}

Se emiten cuando se ejecuta la compacción, manual o automática.

{"type": "compaction_start", "reason": "threshold"}

El campo reason es "manual", "threshold" o "overflow".

{
"type": "compaction_end",
"reason": "threshold",
"result": {
"summary": "Summary of conversation...",
"firstKeptEntryId": "abc123",
"tokensBefore": 150000,
"estimatedTokensAfter": 32000,
"details": {}
},
"aborted": false,
"willRetry": false
}

Si reason fue "overflow" y la compacción tiene éxito, willRetry es true y el agente reintentará automáticamente el prompt.

Si la compacción fue abortada, result es null y aborted es true.

Si la compacción falló (p. ej., cuota de API excedida), result es null, aborted es false, y errorMessage contiene la descripción del error.

Se emiten cuando se activa el reintento automático tras un error transitorio (sobrecarga, límite de tasa, 5xx).

{
"type": "auto_retry_start",
"attempt": 1,
"maxAttempts": 3,
"delayMs": 2000,
"errorMessage": "529 {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"Overloaded\"}}"
}
{
"type": "auto_retry_end",
"success": true,
"attempt": 2
}

En fallo final (reintentos máximos excedidos):

{
"type": "auto_retry_end",
"success": false,
"attempt": 3,
"finalError": "529 overloaded_error: Overloaded"
}

Se emite cuando una extensión lanza un error.

{
"type": "extension_error",
"extensionPath": "/path/to/extension.ts",
"event": "tool_call",
"error": "Error message..."
}

Las extensiones pueden solicitar interacción del usuario vía ctx.ui.select(), ctx.ui.confirm(), etc. En modo RPC, estos se traducen a un subprotocolo solicitud/respuesta sobre el flujo base de comandos/eventos.

Hay dos categorías de métodos de UI de extensión:

  • Métodos dialog (select, confirm, input, editor): emiten un extension_ui_request en stdout y bloquean hasta que el cliente envíe de vuelta un extension_ui_response en stdin con el id coincidente.
  • Métodos fire-and-forget (notify, setStatus, setWidget, setTitle, set_editor_text): emiten un extension_ui_request en stdout pero no esperan respuesta. El cliente puede mostrar la información o ignorarla.

Si un método dialog incluye un campo timeout, el lado del agente se auto-resuelve con un valor predeterminado cuando expira el timeout. El cliente no necesita rastrear timeouts.

Algunos métodos de ExtensionUIContext no están soportados o están degradados en modo RPC porque requieren acceso directo al TUI:

  • custom() devuelve undefined
  • setWorkingMessage(), setWorkingIndicator(), setFooter(), setHeader(), setEditorComponent(), setToolsExpanded() son no-ops
  • getEditorText() devuelve ""
  • getToolsExpanded() devuelve false
  • pasteToEditor() delega a setEditorText() (sin manejo de pegado/colapso)
  • getAllThemes() devuelve []
  • getTheme() devuelve undefined
  • setTheme() devuelve { success: false, error: "..." }

Nota: ctx.mode es "rpc" y ctx.hasUI es true en modo RPC porque los métodos dialog y fire-and-forget funcionan vía el subprotocolo de UI de extensión. Usa ctx.mode === "tui" para proteger funciones específicas del TUI como custom() que requieren un terminal real.

Todas las solicitudes tienen type: "extension_ui_request", un id único y un campo method.

Pedir al usuario que elija de una lista. Los métodos dialog con campo timeout incluyen el timeout en milisegundos; el agente se auto-resuelve con undefined si el cliente no responde a tiempo.

{
"type": "extension_ui_request",
"id": "uuid-1",
"method": "select",
"title": "Allow dangerous command?",
"options": ["Allow", "Block"],
"timeout": 10000
}

Respuesta esperada: extension_ui_response con value (cadena de opción seleccionada) o cancelled: true.

Pedir confirmación sí/no al usuario.

{
"type": "extension_ui_request",
"id": "uuid-2",
"method": "confirm",
"title": "Clear session?",
"message": "All messages will be lost.",
"timeout": 5000
}

Respuesta esperada: extension_ui_response con confirmed: true/false o cancelled: true.

Pedir texto de forma libre al usuario.

{
"type": "extension_ui_request",
"id": "uuid-3",
"method": "input",
"title": "Enter a value",
"placeholder": "type something..."
}

Respuesta esperada: extension_ui_response con value (texto introducido) o cancelled: true.

Abrir un editor de texto multilínea con contenido prefijado opcional.

{
"type": "extension_ui_request",
"id": "uuid-4",
"method": "editor",
"title": "Edit some text",
"prefill": "Line 1\nLine 2\nLine 3"
}

Respuesta esperada: extension_ui_response con value (texto editado) o cancelled: true.

Mostrar una notificación. Fire-and-forget, no se espera respuesta.

{
"type": "extension_ui_request",
"id": "uuid-5",
"method": "notify",
"message": "Command blocked by user",
"notifyType": "warning"
}

El campo notifyType es "info", "warning" o "error". Por defecto "info" si se omite.

Establecer o limpiar una entrada de estado en el pie de página/barra de estado. Fire-and-forget.

{
"type": "extension_ui_request",
"id": "uuid-6",
"method": "setStatus",
"statusKey": "my-ext",
"statusText": "Turn 3 running..."
}

Enviar statusText: undefined (u omitirlo) para limpiar la entrada de estado de esa clave.

Establecer o limpiar un widget (bloque de líneas de texto) mostrado arriba o abajo del editor. Fire-and-forget.

{
"type": "extension_ui_request",
"id": "uuid-7",
"method": "setWidget",
"widgetKey": "my-ext",
"widgetLines": ["--- My Widget ---", "Line 1", "Line 2"],
"widgetPlacement": "aboveEditor"
}

Enviar widgetLines: undefined (u omitirlo) para limpiar el widget. El campo widgetPlacement es "aboveEditor" (predeterminado) o "belowEditor". Solo se soportan arrays de cadenas en modo RPC; las factorías de componentes se ignoran.

Establecer el título de la ventana/pestaña del terminal. Fire-and-forget.

{
"type": "extension_ui_request",
"id": "uuid-8",
"method": "setTitle",
"title": "pi - my project"
}

Establecer el texto en el editor de entrada. Fire-and-forget.

{
"type": "extension_ui_request",
"id": "uuid-9",
"method": "set_editor_text",
"text": "prefilled text for the user"
}

Las respuestas se envían solo para métodos dialog (select, confirm, input, editor). El id debe coincidir con la solicitud.

{"type": "extension_ui_response", "id": "uuid-1", "value": "Allow"}
{"type": "extension_ui_response", "id": "uuid-2", "confirmed": true}

Respuesta de cancelación (cualquier dialog)

Sección titulada «Respuesta de cancelación (cualquier dialog)»

Descartar cualquier método dialog. La extensión recibe undefined (para select/input/editor) o false (para confirm).

{"type": "extension_ui_response", "id": "uuid-3", "cancelled": true}

Los comandos fallidos devuelven una respuesta con success: false:

{
"type": "response",
"command": "set_model",
"success": false,
"error": "Model not found: invalid/model"
}

Errores de parseo:

{
"type": "response",
"command": "parse",
"success": false,
"error": "Failed to parse command: Unexpected token..."
}

Archivos fuente:

{
"id": "claude-sonnet-4-20250514",
"name": "Claude Sonnet 4",
"api": "anthropic-messages",
"provider": "anthropic",
"baseUrl": "https://api.anthropic.com",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 200000,
"maxTokens": 16384,
"cost": {
"input": 3.0,
"output": 15.0,
"cacheRead": 0.3,
"cacheWrite": 3.75
}
}
{
"role": "user",
"content": "Hello!",
"timestamp": 1733234567890,
"attachments": []
}

El campo content puede ser una cadena o un array de bloques TextContent/ImageContent.

{
"role": "assistant",
"content": [
{"type": "text", "text": "Hello! How can I help?"},
{"type": "thinking", "thinking": "User is greeting me..."},
{"type": "toolCall", "id": "call_123", "name": "bash", "arguments": {"command": "ls"}}
],
"api": "anthropic-messages",
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"usage": {
"input": 100,
"output": 50,
"cacheRead": 0,
"cacheWrite": 0,
"cost": {"input": 0.0003, "output": 0.00075, "cacheRead": 0, "cacheWrite": 0, "total": 0.00105}
},
"stopReason": "stop",
"timestamp": 1733234567890
}

Razones de parada: "stop", "length", "toolUse", "error", "aborted"

{
"role": "toolResult",
"toolCallId": "call_123",
"toolName": "bash",
"content": [{"type": "text", "text": "total 48\ndrwxr-xr-x ..."}],
"isError": false,
"timestamp": 1733234567890
}

Creado por el comando RPC bash (no por llamadas a herramientas LLM):

{
"role": "bashExecution",
"command": "ls -la",
"output": "total 48\ndrwxr-xr-x ...",
"exitCode": 0,
"cancelled": false,
"truncated": false,
"fullOutputPath": null,
"timestamp": 1733234567890
}
{
"id": "img1",
"type": "image",
"fileName": "photo.jpg",
"mimeType": "image/jpeg",
"size": 102400,
"content": "base64-encoded-data...",
"extractedText": null,
"preview": null
}
import subprocess
import json
proc = subprocess.Popen(
["pi", "--mode", "rpc", "--no-session"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True
)
def send(cmd):
proc.stdin.write(json.dumps(cmd) + "\n")
proc.stdin.flush()
def read_events():
for line in proc.stdout:
yield json.loads(line)
# Send prompt
send({"type": "prompt", "message": "Hello!"})
# Process events
for event in read_events():
if event.get("type") == "message_update":
delta = event.get("assistantMessageEvent", {})
if delta.get("type") == "text_delta":
print(delta["delta"], end="", flush=True)
if event.get("type") == "agent_end":
print()
break

Consulta test/rpc-example.ts para un ejemplo interactivo completo, o src/modes/rpc/rpc-client.ts para una implementación de cliente tipada.

Para un ejemplo completo de manejo del protocolo de UI de extensión, consulta examples/rpc-extension-ui.ts que se empareja con la extensión examples/extensions/rpc-demo.ts.

const { spawn } = require("child_process");
const { StringDecoder } = require("string_decoder");
const agent = spawn("pi", ["--mode", "rpc", "--no-session"]);
function attachJsonlReader(stream, onLine) {
const decoder = new StringDecoder("utf8");
let buffer = "";
stream.on("data", (chunk) => {
buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
while (true) {
const newlineIndex = buffer.indexOf("\n");
if (newlineIndex === -1) break;
let line = buffer.slice(0, newlineIndex);
buffer = buffer.slice(newlineIndex + 1);
if (line.endsWith("\r")) line = line.slice(0, -1);
onLine(line);
}
});
stream.on("end", () => {
buffer += decoder.end();
if (buffer.length > 0) {
onLine(buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer);
}
});
}
attachJsonlReader(agent.stdout, (line) => {
const event = JSON.parse(line);
if (event.type === "message_update") {
const { assistantMessageEvent } = event;
if (assistantMessageEvent.type === "text_delta") {
process.stdout.write(assistantMessageEvent.delta);
}
}
});
// Send prompt
agent.stdin.write(JSON.stringify({ type: "prompt", message: "Hello" }) + "\n");
// Abort on Ctrl+C
process.on("SIGINT", () => {
agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
});