Skip to content

Clase Agent y ciclo de vida

源码版本v0.73.1

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

  1. Mantener estado: AgentState contiene messages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage; al construir, createMutableAgentState lo inicializa. Ver packages/agent/src/agent.ts:158-188.
  2. Gestión de colas: steeringQueue y followUpQueue son cada uno un PendingMessageQueue, con modo "one-at-a-time" o "all". Ver packages/agent/src/agent.ts:113-144 y packages/agent/src/agent.ts:200-280.
  3. Hooks de ciclo de vida: beforeToolCall/afterToolCall se guardan al construir y se pasan al bucle a través de createLoopConfig. Ver packages/agent/src/agent.ts:410-436.
  4. Envoltorio de ejecución: runWithLifecycle crea el AbortController, fija isStreaming, captura excepciones en handleRunFailure, y en finally ejecuta finishRun. Ver packages/agent/src/agent.ts:438-486.
  5. Tripleta de entrada: prompt recibe nueva entrada, continue retoma desde el final, y steer/followUp encolan. Ver packages/agent/src/agent.ts:312-353 y packages/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

El constructor instala todas las piezas reemplazables; streamFn por defecto apunta a streamSimple:

typescript
// 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:

typescript
// 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:

typescript
// 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/continue lanzan directamente si ya hay activeRun, sin encolar silenciosamente. El llamador debería usar steer/followUp. Ver packages/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". Ver packages/agent/src/agent.ts:336-350.
  • No pierde mensajes ante fallos: handleRunFailure envuelve el error en un AgentMessage de assistant con stopReason: "aborted"|"error" que se añade al transcript; la UI puede renderizarlo. Ver packages/agent/src/agent.ts:463-478.
  • reset limpia todo: reset vacía messages, estado de streaming, pendingToolCalls, errorMessage, y las dos colas. Ver packages/agent/src/agent.ts:301-310.
  • Abort con fuente única: abort() sólo invoca activeRun.abortController.abort(); el bucle y el stream escuchan la misma signal, sin coordinación entre rutas. Ver packages/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.