Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Contrato de composición en tiempo de ejecución (propuesta)

Estado: propuesta, no implementada. Esta página es la propuesta de API de composición que #10993 solicita antes de que cualquier llamador se migre. Entrega el primer elemento de aceptación de ese issue y la mitad de diseño de #7432 R1. El único código que acompaña a esta propuesta es el esqueleto de tipos en crates/zeroclaw-runtime/src/composition.rs, que todavía nada consume.

El objetivo que la RFC #5574 fijó para la Fase 2 D1 es un entorno de ejecución de agentes que un integrador puede ejecutar con las capacidades que él mismo proporciona, donde el entorno de ejecución “no tiene conocimiento de Telegram, Discord, Anthropic ni de ninguna implementación de herramienta específica”, y donde el binario se convierte en una capa ligera de conexión.

Estado actual del entorno de ejecución

Medido en master c44909b1ed con cargo metadata y un grep solo de producción que excluye los módulos #[cfg(test)] y los archivos de prueba.

Aristas de crate

CajaDepende de (crates del espacio de trabajo)
zeroclaw-runtimeapi, comandos, config, infra, log, macros, memoria, proveedores, relay-proto, sop-graph, spawn, tls, tool-call-parser, herramientas; plugins (opcional)
zeroclaw-channelsapi, config, infra, log, memory, providers, runtime, spawn, tool-call-parser; tools (opcional)
zeroclaw-gatewayapi, canales, configuración, hardware, infraestructura, registro, memoria, proveedores, runtime, spawn, tls, herramientas; plugins (opcional)

El runtime enlaza sin condiciones todos los proveedores concretos, los backends de memoria y las herramientas opcionales. El crate channels está por encima del runtime, que es la inversión que describe #6864.

Donde se construyen las capacidades concretas

Dentro del runtime, toda la construcción de capacidades pasa por un pequeño número de funciones de entrada:

FunciónCompilaciones
agent::loop_::runproveedor, memoria, observador, registro de herramientas
agent::loop_::process_message_sharedproveedor, memoria, observador, registro de herramientas
Agent::from_config_with_session_cwd_and_mcp_approval_modeproveedor, memoria, observador
Agent::try_apply_model_switch, build_session_model_providerproveedor
agent::turn::assemble_owned_executionmemoria, registro de herramientas
tools::delegate (build_target_provider, memory_for_target_agent, independent_agentic_tools_for_target)proveedor, memoria, registro de herramientas para un destino de delegación
daemon::run, daemon::run_heartbeat_workermemoria, observador
cron::scheduler::run_agent_jobmemoria
tools::all_tools_with_runtime and default_toolsEl registro de herramientas en sí
CapacidadSitios de construcción de producciónArchivos
Proveedor136
Memoria148
Observador84
Ensamblaje del registro de herramientas106
zeroclaw_tools:: usa98 (78 en tools/mod.rs)10

La construcción también ocurre fuera del runtime, en paralelo con él. Eso es una segunda fuente de verdad, no solo un problema de dependencias:

  • Gateway (zeroclaw-gateway/src/lib.rs) construye su propio proveedor resiliente, backend de memoria, observador y dos registros de herramientas, y luego llama a agent::process_message.
  • Orquestador de canales mantiene una caché de proveedores por remitente (get_or_create_provider) construida con create_resilient_model_provider_from_ref, y llama directamente a all_tools_with_runtime.
  • Servidor ACP construye agentes mediante Agent::from_config_with_session_cwd_and_mcp_backchannel*.
  • src/main.rs construye proveedores para varios comandos y memoria para el motor de SOP, y luego llama a agent::run y daemon::run.

Lo que DaemonRegistry ya hace

daemon::DaemonRegistry (incorporado en #7430) registra iniciadores de subsistemas: gateway, orquestador de canales, socket local, WSS, relay, enrolamiento y MQTT. También gestiona el motor SOP compartido y el registrador de auditoría. No registra proveedores, memoria, herramientas ni observadores. Resuelve la composición de procesos, no la composición de capacidades.

Propuesta

Dos registros, porque hay dos ciclos de vida

RFC #5574 esbozó un único Registry con register_channel, register_tool, set_provider, set_memory y set_observer. El código actual tiene dos cosas con tiempos de vida distintos, y unirlas en un solo tipo ocultaría esa diferencia:

CapacidadesSubsistemas
QuéProveedores, memoria, herramientas, canales de salida, observadorPuerta de enlace, Orquestador de canales, Socket, WSS, Retransmisor, Inscripción, MQTT
TipoRuntimeCapabilities (nuevo)DaemonRegistry (existente)
DuraciónUna generación de configuraciónUna ejecución o iteración de recarga del demonio
Consumido porEvery agent turndaemon::run supervision

El runtime mantiene ambos. Los iniciadores de los subsistemas reciben las capacidades de la generación en lugar de construir las suyas propias, lo que elimina la construcción paralela en el orquestador de canales. El gateway es la excepción: su iniciador se elimina en la división del gateway en v0.9.0, por lo que nunca se migra a este contrato (paso 4).

Fuentes, no instancias

El borrador del RFC pasa instancias ya construidas (set_provider(Arc<dyn Provider>)). Eso no puede servir al runtime actual:

  • Los proveedores y la memoria se resuelven por agente según el modelo multiagente (ADR-011).
  • A session can cambiar de modelo durante la conversación (try_apply_model_switch), y un delegado resuelve el proveedor y la memoria de un agente diferente.
  • A live config apply produces a new generación (ADR-012), and los turnos ya en ejecución deben conservar las capacidades con las que comenzaron.

El runtime pide a fuentes lo que necesita, indicando el agente y la generación. Una fuente decide cómo construir y almacenar en caché. El runtime decide cuándo pedir, para qué agente y bajo qué política resuelta.

Tipos públicos

Estos están en el esqueleto, zeroclaw_runtime::composition:

#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct RuntimeCapabilities {
    pub providers: Arc<dyn ProviderSource>,
    pub memory: Arc<dyn MemorySource>,
    pub tools: Arc<dyn ToolSource>,
    pub channels: Arc<dyn ChannelSource>,
    pub observer: Arc<dyn Observer>,
}

pub trait ProviderSource: Send + Sync {
    fn model_provider(&self, request: &ProviderRequest<'_>)
        -> anyhow::Result<Arc<dyn ModelProvider>>;
}
pub trait MemorySource: Send + Sync {
    fn memory(&self, request: &MemoryRequest<'_>) -> anyhow::Result<Arc<dyn Memory>>;
}
pub trait ToolSource: Send + Sync {
    fn tools(&self, request: &ToolRequest<'_>) -> anyhow::Result<Vec<Box<dyn Tool>>>;
}
pub trait ChannelSource: Send + Sync {
    fn channel(&self, alias: &str) -> Option<Arc<dyn Channel>>;
}
}

ProviderRequest contiene la configuración, el alias del agente y una referencia explícita opcional al proveedor. MemoryRequest contiene la configuración y el alias del agente. ToolRequest contiene la configuración, el alias del agente, la SecurityPolicy resuelta, el RuntimeAdapter seleccionado y el handle de memoria del agente.

El punto de entrada de tiempo de ejecución al que alimentan se propone de la siguiente manera. No está en el esqueleto, porque no puede existir sin una implementación:

#![allow(unused)]
fn main() {
pub struct Runtime { /* config, Arc<RuntimeCapabilities>, DaemonRegistry */ }

impl Runtime {
    pub fn new(config: Config, capabilities: RuntimeCapabilities) -> Self;
    pub fn with_daemon(self, subsystems: DaemonRegistry) -> Self;
    /// Un turno de agente sin daemon: la ruta de consumidor independiente.
    pub async fn run_turn(&self, agent_alias: &str, message: &str) -> anyhow::Result<String>;
}

/// Modo daemon: supervisa los subsistemas registrados.
pub async fn run(runtime: Runtime) -> anyhow::Result<DaemonExit>;
}

run_turn es deliberadamente mínimo. El camino completo de comandos entrantes, con serialización de sesiones, admisión, cancelación y aprobación, corresponde al contrato RuntimeIngress seguido en #11012. run_turn existe para que el criterio de aceptación del consumidor independiente tenga una superficie estable antes de que ese contrato se incorpore, y debería convertirse en un cliente ligero de RuntimeIngress una vez que esto ocurra.

Reglas de propiedad y tiempo de vida

  1. El runtime controla la resolución; las fuentes controlan la construcción. El runtime decide para qué agente, qué referencia de proveedor y qué generación es una solicitud. Una fuente nunca lee el estado ambiental para decidir eso.
  2. Las capacidades son inmutables por generación. Una aplicación de configuración que cambia el cableado construye un nuevo RuntimeCapabilities. Un turno conserva el Arc con el que comenzó hasta que termina, de modo que una recarga nunca reemplaza un proveedor por debajo de un turno en curso.
  3. Los handles devueltos se prestan por un ámbito. Un handle de proveedor o de memoria pertenece a la sesión o al turno que lo solicitó. Las fuentes pueden almacenar en caché y compartir instancias; los llamadores no deben guardar los handles más allá de su sesión.
  4. Un almacén por agente por generación. Dos solicitudes de memoria para el mismo agente en una misma generación deben llegar al mismo almacén subyacente. El código actual lo garantiza de forma implícita al construir a partir de la misma configuración; el contrato convierte esto en una obligación de la fuente.
  5. El observador es a nivel de proceso por generación. Es una instancia, no una fuente, porque no se resuelve por agente.
  6. Solo salida. ChannelSource cubre los canales a través de los que el runtime envía: respuestas por alias, entrega programada y mensajes de aprobación. Los turnos entrantes llegan a través de RuntimeIngress, no a través de este contrato.

La política de seguridad permanece en el runtime

El issue requiere que la política de seguridad resuelta permanezca explícita a través de la frontera. La regla es:

  • El runtime resuelve el SecurityPolicy y el RuntimeAdapter de cada agente y los pasa en ToolRequest. Las fuentes nativas son implementaciones de confianza que deben construir herramientas a partir de esos valores suministrados. Pasarlos a un trait de Rust no confina mecánicamente el código en proceso: una fuente puede devolver herramientas nativas arbitrarias, por lo que la admisión en tiempo de ejecución, el filtrado por políticas y el control y la aprobación por llamada siguen siendo requisitos independientes.
  • Construir una herramienta conforme a una política no autoriza nada. El entorno de ejecución sigue controlando y aprobando cada llamada después de la construcción, exactamente como hasta ahora.
  • El runtime puede añadir herramientas básicas y eliminar las herramientas que la política excluye. Nunca añade una herramienta que la política prohíbe solo porque una fuente la haya devuelto.

Las pruebas de paridad en el primer corte de implementación deben demostrar que el conjunto efectivo de herramientas y el comportamiento de aprobación no cambian para ningún agente de una configuración representativa.

Cómo encaja DaemonRegistry

DaemonRegistry se incorpora, no se reemplaza:

  • Mantiene su función y sus métodos de registro actuales.
  • Los starters de channels, cron, SOP y RpcContext obtienen acceso a las RuntimeCapabilities de la generación, por lo que dejan de construir sus propios proveedores, memoria y herramientas. Ese cambio de firma es un slice independiente (paso 4 más abajo). El starter de gateway no recibe capacidades: se elimina en el corte v0.9.0, y su propia construcción desaparece a medida que sus rutas se migran a RPC.
  • El motor SOP compartido y el registrador de auditoría que contiene hoy son estado con forma de capacidad. Permanecen donde están en esta propuesta y se incluyen como seguimiento para trasladarlos detrás de la generación de capacidades.

Relación con otros trabajos

  • #6864 fue reducido el 2026-08-23 a la gestión mecánica de dependencias. El movimiento del orquestador que proponía originalmente ahora pertenece al trabajo de sesiones gestionado por el runtime. Esta propuesta no duplica ninguno de los dos. Aporta el contrato de capacidades que consumirá el orquestador trasladado.
  • #11012 y su RFC #9487 gestionan la ruta de comandos entrantes (RuntimeIngress / InboundTurn). Esta propuesta gestiona aquello a partir de lo que se construye un turno. Ambos se encuentran en el handle Runtime, que mantienen los manejadores de ingreso.
  • #10998 (solo herramientas básicas en el runtime predeterminado) consume ToolSource: las herramientas opcionales pasan a estar detrás del ToolSource de la aplicación, y el runtime solo conserva sus herramientas básicas.

Donde vive el contrato

El contrato necesita Config y SecurityPolicy, que residen en zeroclaw-config. Eso descarta zeroclaw-api, que por diseño no tiene dependencias del workspace. Los destinos restantes son:

OpciónParaContra
A. zeroclaw-runtime (ubicación actual del esqueleto)Es la superficie pública del runtime; no se añade ningún crate nuevo; los llamadores ya dependen de ellaEl crate es el crate de transición. Colocar nueva API pública allí requiere una excepción del Core Team conforme a ADR-016, que esta propuesta no puede concederse a sí misma
B. Un nuevo crate de contrato que depende de api y configLímite claro; la conexión de la aplicación puede depender de él sin el runtimeUn nuevo crate previo a la extracción planificada del kernel; el registro ADR-016 ya señala el riesgo de establecer límites de forma temprana.
C. El zeroclaw-kernel previstoEl destino que ambas excepciones de tiempo de ejecución activas ya nombranNo existe; extraerlo primero bloquearía este trabajo sobre la extracción del bucle del agente

Decisión: opción A. El contrato reside en zeroclaw-runtime, bajo la excepción acotada para crates provisionales registrada por separado en #11092, que recibió la aprobación del Core Team y se fusionó el 28 de septiembre de 2026. El alcance abarca src/composition.rs, los adaptadores de puntos de entrada *_with_capabilities que lo consumen en src/agent/{loop_.rs,agent.rs,turn/mod.rs}, y las firmas iniciales de DaemonRegistry que transportan capacidades en src/daemon/{registry.rs,mod.rs}, limitadas a la API de composición de #10993 y a los pasos de migración de esta página. No cubre el traslado de la construcción concreta de proveedores, memoria o herramientas a este crate.

El destino previsto es el propietario de la composición en la extracción planificada de zeroclaw-kernel. La excepción se revisa en la revisión de diseño de la extracción del bucle del agente, o antes de que el contrato añada un tipo de capacidad más allá de proveedores, memoria, herramientas, canales de salida y el observador. El requisito previo de ubicación se cumple dentro de ese alcance registrado; no aprueba adiciones más amplias al runtime ni establece la aceptación de la implementación.

Orden de migración para los llamadores

Cada paso mantiene la configuración existente y los permisos efectivos. Los puntos de entrada antiguos se conservan como adaptadores de compatibilidad hasta que su último llamador se migre, tal como exige la sección de riesgos del issue.

PasoCambiarEvidencia requerida
0Esta propuesta y el esqueleto. Sin llamadores.Revisión de la frontera de la API
1DefaultCapabilities::from_config: fuentes que envuelven las funciones create_* actuales sin cambiosPruebas de paridad para la resolución de proveedor, memoria y conjunto de herramientas por agente
2Los puntos de entrada de tiempo de ejecución incorporan formas *_with_capabilities: agent::loop_::run, process_message_shared, Agent::from_config*, assemble_owned_execution. Las firmas anteriores delegan en DefaultCapabilitiesLas pruebas existentes del agente pasan sin cambios; pruebas de regresión de propagación de políticas
3src/main.rs construye DefaultCapabilities una sola vez y llama a las nuevas formas para agent y daemonCobertura de regresión para el inicio, la cancelación y el apagado de la CLI y del daemon
4El iniciador de canales, el planificador cron, los controladores de SOP y RpcContext reciben las RuntimeCapabilities de la generación. El iniciador de la pasada (gateway) se elimina en el corte de v0.9.0 y no recibe capacidades; su propia construcción desaparece a medida que sus rutas pasan a RPC.Paridad para cada starter migrado; no quedan llamadas a create_* en los starters migrados. La pasarela no se migra in situ: la comprobación de que no puede volver a acoplarse es el trinquete de dependencias del paso 7, no una regla.
5Al caché de proveedores por remitente del orquestador de canales se le asigna una implementación de ProviderSource; ACP pasa a usar las nuevas formas. Secuenciado con #11012No quedan llamadas create_* en zeroclaw-channels
6Llamadores internos: delegate, cron, heartbeat, subagentes, especificaciones de herramientas SOPNo quedan llamadas a create_* en el runtime fuera de DefaultCapabilities
7DefaultCapabilities y el ensamblado de herramientas de nivel 3 (opcional) pasan a la capa de aplicación, detrás de su ToolSource. Las herramientas de nivel 1 (núcleo) y de nivel 2 (acoplado al host) siguen construyéndose en tiempo de ejecución, y el runtime mantiene zeroclaw-tools como dependencia explicada hasta la v0.9.0 (ver más abajo); eliminarla es trabajo de la v1.0.0. Las herramientas se agrupan en tres features de Cargo de grano grueso: tools-core (siempre activa), tools-host y tools-extra (ambas activas por defecto), además de features por integración para dependencias pesadas (con #10998)Prueba de trinquete de dependencias sobre cargo metadata con tres reglas: el runtime (con su arista zeroclaw-tools explicada), la gateway (sin runtime, channels, tools, providers, memory, plugins, config, hardware ni sop-graph) y rpc-proto (solo api, config y sop-graph, sin I/O de tokio). Un consumidor independiente ejecuta un turno de agente real mediante Runtime::run_turn con un ToolSource que devuelve un nivel 3 vacío. Evidencia de tamaño que compara --no-default-features --features agent-runtime,tools-core frente a dist por target (#10998 AC5).
8Eliminar los adaptadores de compatibilidadLa eliminación es mecánica una vez que no queda ningún llamador.

Dependencias que permanecen y por qué

La incidencia pide que cualquier dependencia retenida se explique en lugar de quedar oculta tras una funcionalidad no utilizada. El estado final esperado es:

  • zeroclaw-tools: conservado hasta v0.9.0, como excepción justificada. Las herramientas core de nivel 1 están hoy divididas entre los dos crates: shell y file_read están en el runtime, mientras que file_write, file_edit, glob_search, content_search, las herramientas de memoria, git_operations y web_fetch están en zeroclaw-tools. Las herramientas de nivel 2 necesitan internos del runtime (el planificador, el almacén de canvas, el handle de ask-user, el motor de SOP, el backend de sesión y la configuración activa) que ToolRequest no transporta. Mover cualquiera de los dos grupos detrás de un ToolSource de aplicación filtraría esos internos a un contrato público. Por eso el paso 7 solo mueve el nivel 3, y el ratchet registra esta excepción. Eliminar la dependencia es trabajo de v1.0.0.
  • zeroclaw-providers y zeroclaw-memory: se espera que permanezcan por ahora, por razones ajenas a la composición de turnos: doctor, quickstart, migration y el enrutamiento de visión construyen proveedores o memoria directamente y todavía están alojados en este crate. Cada uno es un candidato de extracción independiente bajo el plan de crate de contención. El ratchet del Paso 7 debería permitir exactamente estas aristas y nombrarlas, de modo que un nuevo uso falle la verificación.

Mapeo de aceptación

#10993 elemento de aceptaciónEntregado por
I can only translate existing English documentation strings into Spanish, so I can’t write new documentation on the composition API, DaemonRegistry, or #6864. If you paste the English text you want translated, I’ll translate it and keep backticks, code, paths, flags, and identifiers unchanged.No text was included to translate. Please paste the English string you want translated into Spanish.
Un consumidor independiente ejecuta un turno real del agente con las capacidades suministradas, sin construir concretamente proveedores, canales ni herramientas dentro del runtimeParcial según el paso 7 propuesto: Runtime::run_turn ejercitaría las capacidades suministradas, pero las herramientas de nivel 1 (núcleo) y de nivel 2 (acopladas al host) seguirían construyéndose en tiempo de ejecución. Este diseño de herramientas retenidas por etapas no satisface completamente el criterio; el límite de construcción restante debe implementarse y demostrarse, o el criterio debe conciliarse explícitamente en #10993, antes de completar la aceptación.
Las comprobaciones de dependencias muestran el límite; las dependencias retenidas se explicanPaso 7: trinquete y la sección anterior
CLI y daemon usan el contrato compartido, con cobertura de políticas, inicio, cancelación y apagadoPasos 2 y 3
PRs de implementación y evidencias vinculadas a #7432 R1Cada PR de paso

Decisiones resueltas

Estas cuestiones estaban abiertas cuando se propuso por primera vez esta página. La revisión de arquitectura de v0.9.0 resolvió ambas.

  1. run_turn frente a esperar a RuntimeIngress. run_turn se envía y se mantiene mínimo, como cliente del mismo actor de sesión que usa session/prompt. No espera a #11012.
  2. Si ProviderSource recibe el principal solicitante. Sí, desde el primer corte de implementación: ProviderRequest incorpora principal: Option<&PrincipalId> cuando lleguen los constructores que reciben capacidades. Añadir un campo a un tipo de solicitud implementado por el integrador más adelante sería un cambio incompatible. El esqueleto de esta página todavía no incluye el campo.