Clase Agent y ciclo de vida
Agent es la entrada con estado de @mariozechner/pi-agent-core hacia el exterior. Por debajo está el bucle desnudo runAgentLoop; por arriba, una capa de orquestación como AgentSession. Agent está en el medio y se ocupa de: mantener el transcript y la tabla de herramientas, gestionar el ciclo de vida de activeRun, mantener las dos colas de tareas steering/followUp, traducir AgentOptions a AgentLoopConfig, envolver la llamada real al bucle con runWithLifecycle, y enviar eventos a listeners externos vía subscribe. Se puede entender como una "caja de estado sobre el bucle"; el bucle en sí no guarda nada.
Responsabilidades
- Mantener estado:
AgentStatecontienemessages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage; al construir,createMutableAgentStatelo inicializa. Verpackages/agent/src/agent.ts:158-188. - Gestión de colas:
steeringQueueyfollowUpQueueson cada uno unPendingMessageQueue, con modo"one-at-a-time"o"all". Verpackages/agent/src/agent.ts:113-144ypackages/agent/src/agent.ts:200-280. - Hooks de ciclo de vida:
beforeToolCall/afterToolCallse guardan al construir y se pasan al bucle a través decreateLoopConfig. Verpackages/agent/src/agent.ts:410-436. - Envoltorio de ejecución:
runWithLifecyclecrea elAbortController, fijaisStreaming, captura excepciones enhandleRunFailure, y enfinallyejecutafinishRun. Verpackages/agent/src/agent.ts:438-486. - Tripleta de entrada:
promptrecibe nueva entrada,continueretoma desde el final, ysteer/followUpencolan. Verpackages/agent/src/agent.ts:312-353ypackages/agent/src/agent.ts:252-259.
Motivación de diseño
¿Por qué no dejar que la capa superior llame a runAgentLoop directamente? Porque el bucle es puro: recibe el context, ejecuta y devuelve newMessages; no guarda nada. Un asistente de codificación real necesita "transcript entre turnos, encolar mientras se hace streaming, abort con una única fuente de señal, escribir errorMessage en el estado cuando falla". Meter todo eso en runLoop lo haría pesado y no reutilizable; ponerlo en la capa superior duplicaría código entre los modos print/rpc/TUI. Agent se abstrae como la única caja de estado: el bucle sólo corre, el estado sólo se guarda.
Las colas son dos y no una, porque steering (interrupción del turno actual, a inyectar antes de la siguiente respuesta del asistente) y followUp (arrancar otro turno cuando uno ya iba a terminar) tienen semánticas distintas: steering se tira en el bucle interno con getSteeringMessages, followUp se tira al final del bucle externo con getFollowUpMessages. Ver bucle while doble. El modo "all"/"one-at-a-time" del PendingMessageQueue deja al llamador decidir entre vaciar todo o tomar un solo elemento.
Archivos clave
packages/agent/src/agent.ts:113-144—PendingMessageQueue:enqueue/drain/clear;drainen modo"all"saca todo, si no, sólo el primero.packages/agent/src/agent.ts:158-207— Campos y constructor declass Agent; por defectostreamFnapunta astreamSimpleytoolExecutiones"parallel".packages/agent/src/agent.ts:312-323— Sobrecargas deprompt: admiteAgentMessage,AgentMessage[], ostring + images; por dentro pasa pornormalizePromptInput→runPromptMessages.packages/agent/src/agent.ts:355-372—normalizePromptInput: convierte string+imágenes en unAgentMessagede user contimestamp.packages/agent/src/agent.ts:374-400—runPromptMessages/runContinuationllaman arunAgentLoop/runAgentLoopContinueenvueltos enrunWithLifecycle.packages/agent/src/agent.ts:410-436—createLoopConfig: armarAgentLoopConfigcon los campos de instancia;getSteeringMessages/getFollowUpMessagescierran sobredrainde las colas.packages/agent/src/agent.ts:438-486—runWithLifecycle+handleRunFailure+finishRun.
El constructor instala todas las piezas reemplazables; streamFn por defecto apunta a streamSimple:
// packages/agent/src/agent.ts:190-207
constructor(options: AgentOptions = {}) {
this._state = createMutableAgentState(options.initialState);
this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
this.transformContext = options.transformContext;
this.streamFn = options.streamFn ?? streamSimple;
// ... getApiKey / onPayload / onResponse / beforeToolCall / afterToolCall ...
this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
this.transport = options.transport ?? "auto";
this.toolExecution = options.toolExecution ?? "parallel";
}createLoopConfig envuelve drain de las colas en funciones async, el bucle tira mensajes a través de estos callbacks:
// packages/agent/src/agent.ts:427-434
getSteeringMessages: async () => {
if (skipInitialSteeringPoll) {
skipInitialSteeringPoll = false;
return [];
}
return this.steeringQueue.drain();
},
getFollowUpMessages: async () => this.followUpQueue.drain(),runWithLifecycle es la envoltura común de todas las entradas de ejecución; si algo falla, handleRunFailure escribe el error en el state y emite agent_end:
// packages/agent/src/agent.ts:454-461
try {
await executor(abortController.signal);
} catch (error) {
await this.handleRunFailure(error, abortController.signal.aborted);
} finally {
this.finishRun();
}Flujo de datos
Ciclo de vida de prompt(text):
Límites y fallos
- Rechaza prompt concurrente:
prompt/continuelanzan directamente si ya hayactiveRun, sin encolar silenciosamente. El llamador debería usarsteer/followUp. Verpackages/agent/src/agent.ts:316-320. - Validación de rol en continue: si el último mensaje es
assistant, primero consume las colas steering/followUp; si ambas están vacías lanza "Cannot continue from message role: assistant". Verpackages/agent/src/agent.ts:336-350. - No pierde mensajes ante fallos:
handleRunFailureenvuelve el error en unAgentMessagede assistant constopReason: "aborted"|"error"que se añade al transcript; la UI puede renderizarlo. Verpackages/agent/src/agent.ts:463-478. resetlimpia todo:resetvacía messages, estado de streaming, pendingToolCalls, errorMessage, y las dos colas. Verpackages/agent/src/agent.ts:301-310.- Abort con fuente única:
abort()sólo invocaactiveRun.abortController.abort(); el bucle y el stream escuchan la misma signal, sin coordinación entre rutas. Verpackages/agent/src/agent.ts:287-290.
Resumen
Agent envuelve runAgentLoop en un objeto con estado: mantiene el transcript, gestiona dos colas, instala hooks de ciclo de vida y unifica la señal de abort. Hacia arriba es la capa de orquestación AgentSession; hacia abajo está el bucle while doble. Cómo se consumen los hooks en la ruta de ejecución de herramientas en ejecución de herramientas sequential/parallel; los campos de estado y las formas de AgentMessage/AgentEvent en contrato de tipos.