Aller au contenu principal

C-01 Runtime d'agents

Brique C: Agents, autonomie, exécution. Version anglaise: C-01-agent-runtime

1. Rôle et responsabilités​

Exécute les agents déclarés par un Agent Manifest selon l'architecture Navin : résolution de la version, prise en charge des messages, tour d'exécution (appel modèle puis appels d'outils jusqu'à la réponse finale), appels modèles via le Model Gateway, appels d'outils via le Tool Gateway, émission des étapes dans le contrat d'exécution.

  • Charger l'agent et sa version immuable (eai.agent_versions)
  • Démarrer un run (état pending puis running) et appliquer les budgets
  • AgentLoop (enterprise_ai.loop) : intake des messages (bus, runs en attente, sessions suivies), contexte, sessions, reprise après crash, arrêt, cron, heartbeat, serve()
  • AgentRunner (enterprise_ai.runtime) : un tour complet avec les règles de continuation (travail restant, l'utilisateur a déjà dit go, vérifier avant de finir)
  • Gérer suspend, resume et kill au niveau agent

2. Position dans l'architecture et interactions​

  • Appelants : Portail, SDK, CLI, Channel Gateway (D-13), Scheduler (C-09), Event Triggers (C-10).
  • Chemin d'exécution : création du run synchrone (POST /v1/agents/{id}/runs), puis le processus AgentLoop.serve() du client prend le run en attente et l'exécute ; événements SSE sur GET /v1/runs/{id}/events.
  • Le MessageBus (en mémoire, ou NatsMessageBus sur NATS) relie les canaux au moteur : file entrante lue par serve(), file sortante lue par les canaux.
  • Côté serveur, python -m eai.worker assure le tick du scheduler, le vidage de l'outbox, l'expiration des quotas, les heartbeats périmés et la rétention.
  • Toute interaction passe par l'API Control Plane v1 (/v1) derrière la passerelle API (D-14).

3. Composant open source et partie développée​

  • Moteur : architecture Navin du SDK Python, sans graphe déclaré. enterprise_ai.loop.AgentLoop reçoit les messages, prépare le contexte, gère les sessions, reprend un run après un crash, traite l'arrêt, le cron et le heartbeat, et expose serve() ; enterprise_ai.runtime.AgentRunner exécute un tour ; enterprise_ai.subagents.SubagentManager lance les sous-agents en parallèle ; enterprise_ai.loop.plan.RunPlan porte le plan ; enterprise_ai.runtime.bus.MessageBus relie canaux et moteur.
  • Adaptateurs optionnels : LangGraph (enterprise_ai.runtime.langgraph.build_graph) et Temporal (enterprise_ai.runtime.temporal.build_workflow) réutilisent les mêmes étapes ; ils ne sont pas requis.
  • Côté serveur, python -m eai.worker exécute les tâches périodiques : tick du scheduler, vidage de l'outbox d'événements, expiration des réservations de quota, détection des heartbeats périmés, rétention.
  • Développé : AgentLoop, AgentRunner et ses règles de continuation (enterprise_ai.runtime.continuation.default_rules), SubagentManager, RunPlan, MessageBus, le domaine services/control-plane/src/eai/domains/agents (registre, runs, checkpoints, heartbeats, scheduler, triggers).

4. Modèle de données​

Tables (schéma eai, RLS tenant_id):

  • eai.agents
  • eai.agent_versions
  • eai.runs
  • eai.run_steps

Colonnes communes : id uuid, tenant_id uuid NOT NULL, created_at, updated_at ; politique RLS current_setting('app.tenant_id', true)::uuid.

5. Contrat​

  • Contrat appliqué: agent-manifest + execution-contract
  • Schéma JSON: contracts/v1/agent-manifest.schema.json, contracts/v1/execution-contract.schema.json
  • Champs clés: spec.runtime, spec.models, spec.tools, spec.permissions, spec.autonomy, spec.budgets; run: state, steps, budgets, retry_policy, trace
  • Validation : POST /v1/contracts/{name}/validate ; application d'un manifeste : POST /v1/manifests/apply.

6. Endpoints API​

  • GET /v1/agents
  • POST /v1/agents
  • GET /v1/agents/{id}
  • PATCH /v1/agents/{id}
  • DELETE /v1/agents/{id}
  • GET /v1/agents/{id}/versions
  • POST /v1/agents/{id}/runs
  • GET /v1/runs
  • GET /v1/runs/{id}
  • GET /v1/runs/{id}/events

Requête POST /v1/agents/{id}/runs:

{
"task": {
"input": "Summarize ticket INC-42"
},
"session_id": "ses_7f2",
"budgets": {
"max_steps": 20,
"max_cost": {
"amount": "0.50",
"currency": "EUR"
}
}
}

Réponse:

{
"run_id": "run_01J9",
"state": "queued",
"agent": {
"id": "agt_support",
"version": 3
},
"trace": {
"trace_id": "4bf92f35..."
}
}

7. Exemples SDK​

Python:

from enterprise_ai import EnterpriseAI
from enterprise_ai.loop import AgentLoop
from enterprise_ai.runtime.bus import BusMessage, MessageBus

client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_xxx_secret")
bus = MessageBus()
loop = AgentLoop(client=client, model="claude-sonnet-eu", agent_id="agt_support", bus=bus,
watch_sessions=["ses_7f2"])
result = loop.run_once("Summarize INC-42", session_id="ses_7f2") # one AgentRunner turn
print(result.stop_reason, result.final_text)
loop.resume("run_01J9") # restart a crashed run from its last checkpoint
# asyncio.run(loop.serve()) # intake: bus, pending runs, watched sessions

TypeScript:

import { EnterpriseAI } from "@enterprise-ai/sdk";

const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY });
const agent = await client.agents.create({ manifest });
const run = await client.agents.run(agent.id, { task: { objective: "Summarize INC-42" } });

Java:

EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.build();
var agent = client.agents().create(manifest);
var run = client.runs().start("agt_support", Map.of("task", "Summarize INC-42"));

Go:

c, _ := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")))
_, _ = c.Agents.Create(ctx, manifest)
run, _ := c.Runs.Start(ctx, "agt_support", map[string]any{"task": "Summarize INC-42"})

8. Configuration​

  • Variables d'environnement : EAI_DATABASE_URL, EAI_HEARTBEAT_STALE_SECONDS, EAI_NATS_URL (bus NATS optionnel), EAI_TEMPORAL_URL (adaptateur Temporal optionnel)
  • Clés Helm : controlPlane.replicas, controlPlane.config, worker.enabled, worker.replicas
  • Côté client : options de AgentLoop (model, agent_id, bus, watch_sessions, poll_interval, history_limit, context_budget, budget)

9. Contrôles de sécurité​

  • Scope agents:run requis ; relation OpenFGA can_run sur l'agent
  • Politique OPA agent.run évaluée avant démarrage
  • Secrets jamais injectés dans le prompt ; seulement des leases (D-11)
  • Toutes les requêtes portent le tenant issu du credential ; RLS PostgreSQL
  • Chaque mutation et décision de politique écrit un événement d'audit chaîné

10. Observabilité​

  • Métriques Prometheus: eai_runs_total{state}, eai_run_duration_seconds, eai_run_steps_total
  • Traces: spans OpenTelemetry avec conventions sémantiques GenAI (gen_ai.operation.name, gen_ai.agent.id, gen_ai.tool.name), attributs eai.tenant_id, eai.run_id, propagation traceparent.
  • Actions d'audit: agent.apply, run.create, run.transition

11. Unités FinOps​

  • Tokens modèles par run, nombre d'étapes, durée worker ; attribution agent_id/run_id dans l'Usage Event.
  • Émis comme Usage Event (POST /v1/finops/usage) avec tenant, projet, agent et run.

12. Modes de défaillance et résilience​

  • Crash du processus client : AgentLoop.resume(run_id) relit le dernier checkpoint (GET /v1/runs/{id}/checkpoints) et reprend le tour
  • Arrêt demandé : le heartbeat répond action=stop et AgentRunner s'arrête avant le prochain appel modèle ou outil ; AgentLoop.shutdown() termine le tour courant avant de sortir
  • Idempotency-Key sur POST runs : pas de run en double
  • Dépassement de budget : arrêt du tour avec la raison de la limite atteinte
  • Erreurs normalisées (enveloppe error, message_i18n en/fr)

13. Adoption autonome (client d'un client)​

  • SDK Python enterprise_ai.loop + enterprise_ai.runtime + Control Plane + PostgreSQL ; NATS, LangGraph et Temporal sont optionnels.
  • SDK : enterprise_ai.agent (AgentsClient, RunsClient, SessionsClient), enterprise_ai.loop.AgentLoop, enterprise_ai.runtime.AgentRunner
  • Déploiement minimal : chart Helm avec controlPlane.enabled=true, worker.enabled=true et PostgreSQL ; les composants externes non configurés utilisent leur repli in-process.

14. Tests d'acceptation​

  1. Créer un agent depuis un manifeste valide puis lancer un run
  2. Run refusé sans scope agents:run (403)
  3. Même Idempotency-Key -> même run_id
  4. Accès inter-tenant refusé
  5. Erreur renvoyée en fr avec Accept-Language: fr