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.
Iniciar el modo RPC
Sección titulada «Iniciar el modo RPC»pi --mode rpc [options]Opciones comunes:
--provider <name>: Establecer el proveedor LLM (anthropic, openai, google, etc.)--model <pattern>: Patrón o ID del modelo (soportaprovider/idy 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
Resumen del protocolo
Sección titulada «Resumen del protocolo»- 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.
Enmarcado
Sección titulada «Enmarcado»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\neliminando un\rfinal - 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.
Comandos
Sección titulada «Comandos»Prompting
Sección titulada «Prompting»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.
follow_up
Sección titulada «follow_up»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}new_session
Sección titulada «new_session»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}}get_state
Sección titulada «get_state»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.
get_messages
Sección titulada «get_messages»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).
set_model
Sección titulada «set_model»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": {...}}cycle_model
Sección titulada «cycle_model»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.
get_available_models
Sección titulada «get_available_models»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": [...] }}Pensamiento
Sección titulada «Pensamiento»set_thinking_level
Sección titulada «set_thinking_level»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}cycle_thinking_level
Sección titulada «cycle_thinking_level»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"}}Modos de cola
Sección titulada «Modos de cola»set_steering_mode
Sección titulada «set_steering_mode»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}set_follow_up_mode
Sección titulada «set_follow_up_mode»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}Compacción
Sección titulada «Compacción»compact
Sección titulada «compact»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.
set_auto_compaction
Sección titulada «set_auto_compaction»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}Reintento
Sección titulada «Reintento»set_auto_retry
Sección titulada «set_auto_retry»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}abort_retry
Sección titulada «abort_retry»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 48drwxr-xr-x ...```Esto significa:
- La salida de bash se incluye en el contexto LLM en el siguiente prompt, no inmediatamente
- Se pueden ejecutar múltiples comandos bash antes de un prompt; todas las salidas se incluirán
- No se emite evento para el
BashExecutionMessageen sí
abort_bash
Sección titulada «abort_bash»Abortar un comando bash en ejecución.
{"type": "abort_bash"}Respuesta:
{"type": "response", "command": "abort_bash", "success": true}get_session_stats
Sección titulada «get_session_stats»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.
export_html
Sección titulada «export_html»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"}}switch_session
Sección titulada «switch_session»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}}get_fork_messages
Sección titulada «get_fork_messages»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..."} ] }}get_last_assistant_text
Sección titulada «get_last_assistant_text»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.
set_session_name
Sección titulada «set_session_name»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.
Comandos
Sección titulada «Comandos»get_commands
Sección titulada «get_commands»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íapi.registerCommand()en una extensión"prompt": Cargado desde un archivo plantilla.md"skill": Cargado desde un directorio skill (nombre con prefijoskill:)
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.
Eventos
Sección titulada «Eventos»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).
Tipos de evento
Sección titulada «Tipos de evento»| Event | Description |
|---|---|
agent_start | El agente comienza a procesar |
agent_end | El agente completa (incluye todos los mensajes generados) |
turn_start | Comienza un nuevo turno |
turn_end | El turno completa (incluye mensaje del assistant y resultados de herramientas) |
message_start | Comienza un mensaje |
message_update | Actualización en streaming (deltas de text/thinking/toolcall) |
message_end | Completa un mensaje |
tool_execution_start | La herramienta comienza ejecución |
tool_execution_update | Progreso de ejecución de herramienta (salida en streaming) |
tool_execution_end | La herramienta completa |
queue_update | Cambió la cola pendiente de steering/follow-up |
compaction_start | Comienza la compacción |
compaction_end | Completa la compacción |
auto_retry_start | Comienza reintento automático (tras error transitorio) |
auto_retry_end | Completa reintento automático (éxito o fallo final) |
extension_error | La extensión lanzó un error |
agent_start
Sección titulada «agent_start»Se emite cuando el agente comienza a procesar un prompt.
{"type": "agent_start"}agent_end
Sección titulada «agent_end»Se emite cuando el agente completa. Contiene todos los mensajes generados durante esta ejecución.
{ "type": "agent_end", "messages": [...]}turn_start / turn_end
Sección titulada «turn_start / turn_end»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": [...]}message_start / message_end
Sección titulada «message_start / message_end»Se emiten cuando un mensaje comienza y completa. El campo message contiene un AgentMessage.
{"type": "message_start", "message": {...}}{"type": "message_end", "message": {...}}message_update (Streaming)
Sección titulada «message_update (Streaming)»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:
| Type | Description |
|---|---|
start | Comenzó la generación del mensaje |
text_start | Comenzó el bloque de contenido de texto |
text_delta | Fragmento de contenido de texto |
text_end | Terminó el bloque de contenido de texto |
thinking_start | Comenzó el bloque de pensamiento |
thinking_delta | Fragmento de contenido de pensamiento |
thinking_end | Terminó el bloque de pensamiento |
toolcall_start | Comenzó la llamada a herramienta |
toolcall_delta | Fragmento de argumentos de llamada a herramienta |
toolcall_end | Terminó la llamada a herramienta (incluye objeto toolCall completo) |
done | Mensaje completo (razón: "stop", "length", "toolUse") |
error | Ocurrió 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.
queue_update
Sección titulada «queue_update»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"]}compaction_start / compaction_end
Sección titulada «compaction_start / compaction_end»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.
auto_retry_start / auto_retry_end
Sección titulada «auto_retry_start / auto_retry_end»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"}extension_error
Sección titulada «extension_error»Se emite cuando una extensión lanza un error.
{ "type": "extension_error", "extensionPath": "/path/to/extension.ts", "event": "tool_call", "error": "Error message..."}Protocolo de UI de extensión
Sección titulada «Protocolo de UI de extensión»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 unextension_ui_requesten stdout y bloquean hasta que el cliente envíe de vuelta unextension_ui_responseen stdin con elidcoincidente. - Métodos fire-and-forget (
notify,setStatus,setWidget,setTitle,set_editor_text): emiten unextension_ui_requesten 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()devuelveundefinedsetWorkingMessage(),setWorkingIndicator(),setFooter(),setHeader(),setEditorComponent(),setToolsExpanded()son no-opsgetEditorText()devuelve""getToolsExpanded()devuelvefalsepasteToEditor()delega asetEditorText()(sin manejo de pegado/colapso)getAllThemes()devuelve[]getTheme()devuelveundefinedsetTheme()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.
Solicitudes de UI de extensión (stdout)
Sección titulada «Solicitudes de UI de extensión (stdout)»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.
confirm
Sección titulada «confirm»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.
setStatus
Sección titulada «setStatus»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.
setWidget
Sección titulada «setWidget»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.
setTitle
Sección titulada «setTitle»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"}set_editor_text
Sección titulada «set_editor_text»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"}Respuestas de UI de extensión (stdin)
Sección titulada «Respuestas de UI de extensión (stdin)»Las respuestas se envían solo para métodos dialog (select, confirm, input, editor). El id debe coincidir con la solicitud.
Respuesta de valor (select, input, editor)
Sección titulada «Respuesta de valor (select, input, editor)»{"type": "extension_ui_response", "id": "uuid-1", "value": "Allow"}Respuesta de confirmación (confirm)
Sección titulada «Respuesta de confirmación (confirm)»{"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}Manejo de errores
Sección titulada «Manejo de errores»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:
packages/ai/src/types.ts-Model,UserMessage,AssistantMessage,ToolResultMessagepackages/agent/src/types.ts-AgentMessage,AgentEventsrc/core/messages.ts-BashExecutionMessagesrc/modes/rpc/rpc-types.ts- Tipos de comando/respuesta RPC, tipos de solicitud/respuesta de UI de extensión
{ "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 }}UserMessage
Sección titulada «UserMessage»{ "role": "user", "content": "Hello!", "timestamp": 1733234567890, "attachments": []}El campo content puede ser una cadena o un array de bloques TextContent/ImageContent.
AssistantMessage
Sección titulada «AssistantMessage»{ "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"
ToolResultMessage
Sección titulada «ToolResultMessage»{ "role": "toolResult", "toolCallId": "call_123", "toolName": "bash", "content": [{"type": "text", "text": "total 48\ndrwxr-xr-x ..."}], "isError": false, "timestamp": 1733234567890}BashExecutionMessage
Sección titulada «BashExecutionMessage»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}Attachment
Sección titulada «Attachment»{ "id": "img1", "type": "image", "fileName": "photo.jpg", "mimeType": "image/jpeg", "size": 102400, "content": "base64-encoded-data...", "extractedText": null, "preview": null}Ejemplo: Cliente básico (Python)
Sección titulada «Ejemplo: Cliente básico (Python)»import subprocessimport 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 promptsend({"type": "prompt", "message": "Hello!"})
# Process eventsfor 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() breakEjemplo: Cliente interactivo (Node.js)
Sección titulada «Ejemplo: Cliente interactivo (Node.js)»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 promptagent.stdin.write(JSON.stringify({ type: "prompt", message: "Hello" }) + "\n");
// Abort on Ctrl+Cprocess.on("SIGINT", () => { agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");});