Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Contrat de composition à l’exécution (proposition)

Statut : proposé, non implémenté. Cette page présente la proposition d’API de composition demandée par #10993 avant que tout appelant ne soit migré. Elle livre le premier critère d’acceptation de cette issue ainsi que le volet conception de #7432 R1. Le seul code qui l’accompagne est le squelette de types dans crates/zeroclaw-runtime/src/composition.rs, que rien n’utilise encore.

L’objectif fixé par la RFC #5574 pour Phase 2 D1 : un runtime d’agent qu’un intégrateur peut exécuter avec les capacités qu’il fournit, où le runtime « ne connaît ni Telegram, ni Discord, ni Anthropic, ni aucune implémentation d’outil spécifique », et où le binaire devient une couche de câblage légère.

Où en est l’exécution actuellement

Mesuré sur master c44909b1ed avec cargo metadata et un grep limité au code de production, qui exclut les modules #[cfg(test)] et les fichiers de test.

Bords de caisse

CrateDépend de (crates de l’espace de travail)
zeroclaw-runtimeapi, commandes, config, infra, journal, macros, mémoire, fournisseurs, relay-proto, sop-graph, spawn, tls, tool-call-parser, outils ; plugins (optionnel)
zeroclaw-channelsapi, config, infra, log, mémoire, fournisseurs, runtime, spawn, tool-call-parser ; outils (facultatif)
zeroclaw-gatewayapi, canaux, configuration, matériel, infrastructure, journal, mémoire, fournisseurs, runtime, lancement de processus, tls, outils ; plugins (optionnel)

Le runtime lie inconditionnellement chaque fournisseur concret, chaque backend de mémoire et chaque outil optionnel. Le crate channels se situe au-dessus du runtime, ce qui constitue l’inversion décrite dans #6864.

Là où les capacités concrètes sont construites

Dans le runtime, toute la construction des capacités passe par un petit nombre de fonctions d’entrée :

FonctionBuilds
agent::loop_::runfournisseur, mémoire, observateur, registre d’outils
agent::loop_::process_message_sharedfournisseur, mémoire, observateur, registre d’outils
Agent::from_config_with_session_cwd_and_mcp_approval_modefournisseur, mémoire, observateur
Agent::try_apply_model_switch, build_session_model_providerfournisseur
agent::turn::assemble_owned_executionmémoire, registre d’outils
tools::delegate (build_target_provider, memory_for_target_agent, independent_agentic_tools_for_target)fournisseur, mémoire, registre d’outils pour une cible de délégation
daemon::run, daemon::run_heartbeat_workermémoire, observateur
cron::scheduler::run_agent_jobMémoire
tools::all_tools_with_runtime et default_toolsle registre d’outils lui-même
CapacitéChantiers de productionFichiers
Fournisseur136
Mémoire148
Observateur84
Assemblage du registre d’outils106
zeroclaw_tools:: utiliseј98 (78 dans tools/mod.rs)10

La construction se fait aussi en dehors du runtime, en parallèle de celui-ci. C’est une seconde source de vérité, et pas seulement un problème de dépendance :

  • Gateway (zeroclaw-gateway/src/lib.rs) construit son propre fournisseur résilient, son backend de mémoire, son observateur et deux registres d’outils, puis appelle agent::process_message.
  • Orchestrateur de canaux conserve un cache de fournisseurs par expéditeur (get_or_create_provider) construit avec create_resilient_model_provider_from_ref, et appelle directement all_tools_with_runtime.
  • Serveur ACP construit les agents via Agent::from_config_with_session_cwd_and_mcp_backchannel*.
  • src/main.rs construit les fournisseurs pour plusieurs commandes et la mémoire pour le moteur SOP, puis appelle agent::run et daemon::run.

Ce que DaemonRegistry fait déjà

daemon::DaemonRegistry (introduit dans #7430) enregistre les démarreurs de sous-systèmes : passerelle, orchestrateur des canaux, socket locale, WSS, relais, enrôlement et MQTT. Il gère également le moteur SOP partagé et le journal d’audit. Il n’enregistre ni les fournisseurs, ni la mémoire, ni les outils, ni les observateurs. Il résout la composition des processus, non la composition des capacités.

Proposition

Deux registres, car il existe deux durées de vie

RFC #5574 esquissait un seul Registry avec register_channel, register_tool, set_provider, set_memory et set_observer. Le code actuel comporte deux éléments ayant des durées de vie différentes, et les regrouper dans un seul type masquerait cette distinction :

CapacitésSous-systèmes
QueFournisseurs, mémoire, outils, canaux sortants, observateurPasserelle, orchestrateur de canaux, socket, WSS, relais, inscription, MQTT
TypeRuntimeCapabilities (nouveau)DaemonRegistry (existant)
Durée de vieUne génération de configurationOne exécution du démon ou itération de rechargement
Consommé parChaque tour de l’agentdaemon::run supervision

Le runtime détient les deux. Les démarreurs de sous-système reçoivent les capacités de la génération au lieu d’en construire leurs propres, ce qui supprime la construction parallèle dans l’orchestrateur des canaux. La passerelle fait exception : son démarreur est supprimé lors de la scission de la passerelle en v0.9.0, il n’est donc jamais migré vers ce contrat (étape 4).

Sources, pas instances

L’esquisse de RFC transmet des instances déjà construites (set_provider(Arc<dyn Provider>)). Cela ne peut pas convenir au runtime actuel :

  • Les fournisseurs et la mémoire se résolvent par agent dans le modèle multi-agents (ADR-011).
  • A session can changer de modèle en cours de conversation (try_apply_model_switch), et un délégué résout le fournisseur et la mémoire d’un agent différent.
  • Une application de configuration en direct produit une nouvelle génération (ADR-012), et les tours déjà en cours doivent conserver les capacités avec lesquelles ils ont démarré.

Ainsi, le runtime demande aux sources ce dont il a besoin, en précisant l’agent et la génération. Une source décide comment construire et mettre en cache. Le runtime décide quand demander, pour quel agent, et selon quelle politique résolue.

Types publics

Ceux-ci se trouvent dans le squelette, 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 transporte la configuration, l’alias de l’agent et une référence explicite optionnelle au fournisseur. MemoryRequest transporte la configuration et l’alias de l’agent. ToolRequest transporte la configuration, l’alias de l’agent, le SecurityPolicy résolu, le RuntimeAdapter sélectionné et le handle mémoire de l’agent.

Le point d’entrée d’exécution alimenté par ces éléments est proposé comme suit. Il n’est pas présent dans le squelette, car il ne peut pas exister sans implémentation :

#![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 tour d'agent sans démon : le chemin du consommateur indépendant.
    pub async fn run_turn(&self, agent_alias: &str, message: &str) -> anyhow::Result<String>;
}

/// Mode démon : superviser les sous-systèmes enregistrés.
pub async fn run(runtime: Runtime) -> anyhow::Result<DaemonExit>;
}

run_turn est volontairement minimal. Le chemin complet de commande entrante, avec la sérialisation des sessions, l’admission, l’annulation et l’approbation, relève du contrat RuntimeIngress suivi dans #11012. run_turn existe pour que le critère d’acceptation du consommateur indépendant dispose d’une surface stable avant l’arrivée de ce contrat, et il devra devenir un client léger de RuntimeIngress une fois celui-ci disponible.

Règles de propriété et de durée de vie

  1. Le runtime gère la résolution ; les sources gèrent la construction. Le runtime décide pour quel agent, quelle référence de fournisseur et quelle génération une requête est destinée. Une source ne lit jamais l’état ambiant pour décider de cela.
  2. Les capacités sont immuables par génération. Une application de configuration qui modifie le câblage construit un nouveau RuntimeCapabilities. Un tour conserve l’Arc avec lequel il a démarré jusqu’à sa fin, de sorte qu’un rechargement ne remplace jamais un fournisseur en cours d’utilisation par un tour en cours d’exécution.
  3. Les handles retournés sont empruntés pour une portée. Un handle de fournisseur ou de mémoire appartient à la session ou au tour qui l’a demandé. Les sources peuvent mettre en cache et partager des instances ; les appelants ne doivent pas les conserver au-delà de leur session.
  4. Un seul store par agent par génération. Deux requêtes mémoire pour le même agent dans une même génération doivent atteindre le même store sous-jacent. Le code actuel garantit cela implicitement en construisant à partir de la même configuration ; le contrat en fait une obligation de la source.
  5. L’observateur est au niveau du processus par génération. Il s’agit d’une instance, et non d’une source, car il n’est pas résolu par agent.
  6. Sortant uniquement. ChannelSource couvre les canaux par lesquels le runtime envoie des messages : réponses par alias, livraison planifiée et invites d’approbation. Les tours entrants arrivent par RuntimeIngress, et non par ce contrat.

La politique de sécurité reste gérée par le runtime

Le problème exige que la politique de sécurité résolue reste explicite à travers la frontière. La règle est :

  • Le runtime résout le SecurityPolicy et le RuntimeAdapter de chaque agent et les transmet dans ToolRequest. Les sources natives sont des implémentations de confiance, tenues de construire les outils à partir de ces valeurs fournies. Les transmettre à un trait Rust ne confine pas mécaniquement le code qui s’exécute dans le processus : une source peut renvoyer des outils natifs arbitraires. Ainsi, l’admission à l’exécution, le filtrage des politiques, ainsi que le contrôle et l’approbation par appel restent des exigences indépendantes.
  • Créer un outil à partir d’une politique n’autorise rien. Le runtime continue de filtrer et d’approuver chaque appel après la construction, exactement comme aujourd’hui.
  • Le runtime peut ajouter des outils principaux et supprimer les outils que la politique exclut. Il n’ajoute jamais un outil que la politique interdit, même si une source l’a renvoyé.

Les tests de parité de la première tranche d’implémentation doivent montrer que l’ensemble effectif d’outils et le comportement d’approbation restent inchangés pour chaque agent d’une configuration représentative.

Comment DaemonRegistry s’intègre

DaemonRegistry est intégré, et non remplacé :

  • Il conserve son rôle et ses méthodes d’enregistrement actuelles.
  • Les démarreurs channels, cron, SOP et RpcContext accèdent désormais aux RuntimeCapabilities de la génération, et ne construisent donc plus leurs propres providers, mémoire et outils. Ce changement de signature constitue un lot à part entière (étape 4 ci-dessous). Le démarreur gateway ne reçoit aucune capacité : il est supprimé lors de la coupure v0.9.0, et sa propre construction disparaît à mesure que ses routes migrent vers RPC.
  • Le moteur SOP partagé et le journal d’audit qu’il embarque aujourd’hui sont un état de forme capacitaire. Ils restent à leur emplacement actuel dans cette proposition et sont inscrits comme suivi à faire ultérieurement, afin d’être déplacés derrière la génération de capacités.

Relation avec les autres travaux

  • #6864 a été restreint le 2026-08-23 à la gestion mécanique des dépendances. Le déplacement d’orchestrateur qu’il proposait initialement appartient désormais au travail de session géré par le runtime. Cette proposition ne duplique ni l’un ni l’autre. Elle fournit le contrat de capacités que l’orchestrateur déplacé consommera.
  • #11012 et sa RFC #9487 gèrent le chemin de commande entrant (RuntimeIngress / InboundTurn). Cette proposition gère ce à partir de quoi un tour est construit. Les deux se rejoignent au niveau du handle Runtime, que détiennent les gestionnaires d’entrée.
  • #10998 (outils de base uniquement dans le runtime par défaut) utilise ToolSource : les outils optionnels passent derrière le ToolSource de l’application, et le runtime ne conserve que ses outils de base.

Où réside le contrat

Le contrat a besoin de Config et SecurityPolicy, qui se trouvent dans zeroclaw-config. Cela exclut zeroclaw-api, qui n’a aucune dépendance interne au workspace par conception. Les emplacements restants sont :

OptionPourContre
A. zeroclaw-runtime (emplacement actuel du squelette)Elle est l’interface publique du runtime ; aucune nouvelle crate ; les appelants en dépendent déjàLe crate est le crate de transition. Placer une nouvelle API publique dans ce crate nécessite une exception de la Core Team au titre de l’ADR-016, que cette proposition ne peut pas s’accorder à elle-même.
B. Une nouvelle crate de contrat dépendant de api et configFrontière propre ; le câblage de l’application peut en dépendre sans le runtimeA new crate ahead of the planned kernel extraction; the ADR-016 record already notes the risk of establishing boundaries early
C. Le zeroclaw-kernel prévuLa destination que les deux exceptions d’exécution actives nomment déjàN’existe pas ; extraire celui-ci d’abord bloquerait ce travail sur l’extraction de la boucle d’agent

Décision : option A. Le contrat réside dans zeroclaw-runtime, sous l’exception de crate de transition bornée consignée séparément dans #11092, qui a reçu l’approbation de la Core Team et a été fusionnée le 2026-09-28. Le périmètre couvre src/composition.rs, les adaptateurs de point d’entrée *_with_capabilities qui le consomment dans src/agent/{loop_.rs,agent.rs,turn/mod.rs}, et les signatures de démarrage de DaemonRegistry porteuses de capacités dans src/daemon/{registry.rs,mod.rs}, limité à l’API de composition #10993 et aux étapes de migration de cette page. Il ne couvre pas le déplacement de la construction concrète des fournisseurs, de la mémoire ou des outils dans cette crate.

La destination prévue est le propriétaire de la composition dans l’extraction planifiée de zeroclaw-kernel. L’exception est soumise à examen lors de la revue de conception de l’extraction de la boucle d’agent, ou avant que le contrat n’ajoute un type de capacité au-delà des fournisseurs, de la mémoire, des outils, des canaux sortants et de l’observateur. Le prérequis de placement est satisfait dans le périmètre enregistré ; il n’approuve pas d’ajouts d’exécution plus larges et n’établit pas l’acceptation de l’implémentation.

Ordre de migration des appelants

Chaque étape conserve la configuration existante et les permissions effectives. Les anciens points d’entrée restent en place comme adaptateurs de compatibilité jusqu’à ce que leur dernier appelant soit migré, comme l’exige la section risques de l’issue.

ÉtapeModifierPreuve requise
0Cette proposition et le squelette. Aucun appelant.Revue de la frontière de l’API
1DefaultCapabilities::from_config : sources qui encapsulent les fonctions create_* actuelles sans modificationTests de parité pour la résolution du fournisseur, de la mémoire et de l’ensemble d’outils par agent
2Les points d’entrée d’exécution gagnent des variantes *_with_capabilities : agent::loop_::run, process_message_shared, Agent::from_config*, assemble_owned_execution. Les anciennes signatures délèguent à DefaultCapabilities.Les tests d’agent existants passent sans modification ; tests de régression de la propagation des politiques
3src/main.rs construit DefaultCapabilities une seule fois et appelle les nouvelles formes pour agent et daemonCouverture de tests de non-régression pour le démarrage, l’annulation et l’arrêt de la CLI et du démon
4Le démarreur des canaux, le planificateur cron, les pilotes SOP et RpcContext reçoivent les RuntimeCapabilities de la génération. Le démarreur de la passerelle est supprimé lors de la coupure v0.9.0 et ne reçoit aucune capacité ; sa construction elle-même disparaît, ses routes étant migrées vers RPC.Parité pour chaque starter migré ; aucun appel create_* restant dans les starters migrés. La passerelle n’est pas migrée sur place : la vérification qu’elle ne peut pas se re-coupler est le cliquet de dépendances de l’étape 7, et non une règle
5Le cache de fournisseurs par expéditeur de l’orchestrateur Channels devient une implémentation de ProviderSource ; ACP passe aux nouvelles formes. Séquencé avec #11012Aucun appel create_* restant dans zeroclaw-channels
6Appelants internes : delegate, cron, heartbeat, sous-agents, spécifications d’outils SOPAucun appel create_* ne subsiste dans le runtime en dehors de DefaultCapabilities
7DefaultCapabilities et l’assemblage des outils de niveau 3 (optionnels) passent dans la couche applicative, derrière son ToolSource. Les outils de niveau 1 (cœur) et de niveau 2 (couplés à l’hôte) restent construits à l’exécution, et le runtime conserve zeroclaw-tools comme dépendance documentée jusqu’à la v0.9.0 (voir ci-dessous) ; sa suppression relève du travail de la v1.0.0. Les outils sont regroupés sous trois fonctionnalités Cargo grossières, tools-core (toujours activée), tools-host et tools-extra (toutes deux activées par défaut), ainsi que des fonctionnalités par intégration pour les dépendances lourdes (avec #10998)Test de cliquet sur les dépendances via cargo metadata avec trois règles : le runtime (son lien zeroclaw-tools expliqué), la gateway (ni runtime, channels, tools, providers, memory, plugins, config, hardware, ni sop-graph), et rpc-proto (api, config et sop-graph uniquement, sans E/S tokio). Un consommateur indépendant exécutant un vrai tour d’agent via Runtime::run_turn avec un ToolSource qui renvoie un tier 3 vide. Preuves de taille comparant --no-default-features --features agent-runtime,tools-core à dist par cible (#10998 AC5)
8Supprimer les adaptateurs de compatibilitéLa suppression est mécanique une fois qu’aucun appelant ne subsiste.

Dépendances restantes, et pourquoi

Le ticket demande que toute dépendance conservée soit expliquée plutôt que masquée derrière une fonctionnalité inutilisée. L’état final attendu est :

  • zeroclaw-tools : conservé jusqu’à la v0.9.0, comme exception documentée. Les outils principaux de niveau 1 sont actuellement répartis entre les deux crates : shell et file_read dans le runtime ; file_write, file_edit, glob_search, content_search, les outils de mémoire, git_operations et web_fetch dans zeroclaw-tools. Les outils de niveau 2 ont besoin d’éléments internes du runtime (le planificateur, le magasin de canvas, le handle de question à l’utilisateur, le moteur SOP, le backend de session et la configuration active) que ToolRequest ne transporte pas. Déplacer l’un ou l’autre derrière un ToolSource d’application ferait fuir ces éléments internes dans un contrat public. L’étape 7 ne déplace donc que le niveau 3, et le mécanisme de ratchet désigne explicitement cette exception. Supprimer la dépendance relève de la v1.0.0.
  • zeroclaw-providers et zeroclaw-memory : censés rester en l’état pour le moment, pour des raisons indépendantes de la composition des tours : doctor, quickstart, migration et le routage de vision construisent encore des fournisseurs ou de la mémoire directement, et sont toujours hébergés dans ce crate. Chacun constitue un candidat distinct à l’extraction dans le plan du crate de transit. Le ratchet de l’étape 7 doit autoriser exactement ces dépendances et les nommer explicitement, afin qu’une nouvelle utilisation fasse échouer la vérification.

Mappage d’acceptation

#10993 critère d’acceptationLivré par
Documenter l’API de composition publique, les règles de propriété et de durée de vie, DaemonRegistry, partie pertinente de #6864Cette page (étape 0)
Un consommateur indépendant exécute un véritable tour d’agent avec les capacités fournies, sans construction concrète de fournisseur, de canal ou d’outil à l’intérieur du runtimePartiel selon l’étape 7 proposée : Runtime::run_turn exercerait les capacités fournies, mais les outils de niveau 1 (cœur) et de niveau 2 (couplés à l’hôte) resteraient construits à l’exécution. Cette conception à outils retenus par étapes ne satisfait pas entièrement le critère. La frontière de construction restante doit être implémentée et démontrée, ou le critère explicitement réconcilié dans #10993, avant que l’acceptation ne soit complète.
Les vérifications des dépendances illustrent la limite ; les dépendances conservées sont expliquéesÉtape 7 : cliquet et la section ci-dessus
CLI et démon utilisent le contrat partagé, avec une couverture des politiques, du démarrage, de l’annulation et de l’arrêtJe n’ai pas reçu le texte à traduire. Pouvez-vous coller les étapes 2 et 3 (en anglais) ?
Pull requests d’implémentation et preuves liées à #7432 R1La PR de chaque étape

Décisions arrêtées

Ces points étaient ouverts lorsque cette page a été proposée pour la première fois. La revue d’architecture de la v0.9.0 les a tranchés tous les deux.

  1. run_turn versus waiting for RuntimeIngress. run_turn ships and stays minimal, as a client of the same session actor that session/prompt uses. It does not wait for #11012.
  2. Indique si ProviderSource reçoit le principal demandeur. Oui, dès la première tranche d’implémentation : ProviderRequest gagne principal: Option<&PrincipalId> lorsque les constructeurs prenant une capacité seront introduits. Ajouter un champ à un type de requête implémenté par l’intégrateur plus tard constituerait un changement incompatible. Le squelette de cette page ne porte pas encore ce champ.