veilletech.fr
22 août Feed du jour
#05 LARAVEL Article

Laravel AI 0.11 : voir ce que fait l'agent

Un agent qu'on ne peut pas tracer est un agent qu'on ne peut pas exploiter en production.

Laravel AI 0.11 donne à chaque run d'agent un identifiant de corrélation unique qui survit aux bascules de fournisseur, et des événements de cycle de vie autour de chaque aller-retour et de chaque appel d'outil. Le wrapper ToolSearch permet en plus de ne plus envoyer tout le catalogue d'outils à chaque requête.

4 min de lecturevidéo 1:20
Partager
Sommaire7 sections
  1. Ce qui se passe
  2. Un identifiant qui traverse tout le run
  3. Les événements
  4. Les agents imbriqués
  5. ToolSearch : arrêter d'envoyer tout le catalogue
  6. Le point de mise à jour à ne pas rater
  7. À retenir

Ce qui se passe

Laravel AI 0.11.0 est sorti le 19 août 2026, avec 36 pull requests fusionnées et douze premiers contributeurs. Le thème de la version tient en un mot : observabilité.

Le constat de départ est net. Jusqu'ici, un run qui prenait cinq allers-retours avec le fournisseur pour résoudre ses appels d'outils ressemblait, vu de l'extérieur, à un run résolu en un seul appel. Et un run mort dans la passerelle ne remontait rien du tout.

Un identifiant qui traverse tout le run

Sept pull requests de @pushpak1300 refondent la façon dont un run rend compte de lui-même.

streamPrompt() émettait déjà un identifiant d'invocation au niveau du run, mais pas prompt() — un middleware synchrone voyait donc $prompt->invocationId === null là où un middleware de streaming voyait une vraie valeur. Pire : un failover sur trois fournisseurs produisait trois identifiants sans lien pour ce qui n'était qu'un seul run. prompt() émet désormais l'identifiant dès le départ, et le fournisseur réutilise celui fourni par l'appelant.

Un objet RunContext porte maintenant l'identité du run et distribue ses événements directement, en remplacement d'une paire de callbacks que chaque fournisseur enregistrait sur la boucle de génération. L'ancien montage suivait l'identifiant d'invocation d'outil courant dans une unique propriété mutable, ce qui cassait dès qu'il y avait imbrication : un agent invoqué comme outil écrasait l'identifiant avant que le ToolInvoked extérieur ne parte, si bien que l'événement extérieur portait l'identifiant de l'appel intérieur. L'identifiant est désormais émis à l'intérieur de executeTool(), et les outils le lisent avec Request::toolInvocationId().

Les événements

StartingStep, StepCompleted et StepFailed encadrent chaque aller-retour avec le fournisseur, sur le chemin synchrone comme sur le chemin streaming. StartingStep porte les messages et les options résolues envoyés à l'étape, StepCompleted porte la réponse complète, et chaque événement de fin porte le temps mural en millisecondes, pour s'aligner sur QueryExecuted::$time.

Attention à un détail d'implémentation : StartingStep transporte tout l'historique de messages du run, donc un listener ShouldQueue le sérialisera, pièces jointes comprises.

executeTool() n'avait aucun catch : un outil dont le handler levait une exception la propageait hors de la boucle de génération, sans trace de l'outil en cause. ToolFailed le signale désormais, en portant le même identifiant d'invocation d'outil que le InvokingTool qui l'avait ouvert. L'exception est toujours relancée.

Au niveau du run, AgentFailed signale une défaillance terminale, une seule fois, et seulement une fois le run terminé — c'est-à-dire après épuisement de la chaîne de fournisseurs.

Les agents imbriqués

Un appel d'outil publie maintenant son identifiant de run et son identifiant d'invocation pour toute sa durée. Tout agent sollicité pendant ce temps les récupère sous forme de parentInvocationId et parentToolInvocationId, ce qui rattache un sous-agent au run qui lui a délégué le travail. Le mécanisme couvre aussi un outil écrit à la main qui invoque un agent, pas seulement AgentTool.

La limite est explicite : le lien ne franchit pas la frontière d'une file. Un prompt lancé avec promptOnQueue() depuis l'intérieur d'un outil démarre son propre run, orphelin.

ToolSearch : arrêter d'envoyer tout le catalogue

Deuxième apport notable, signé @behzadsp. Jusqu'ici, chaque outil exposé par un agent était envoyé au fournisseur à chaque requête. Avec un large catalogue, cela coûte des tokens et présente au modèle un menu interminable.

Le wrapper ToolSearch diffère les outils qu'il contient, pour qu'OpenAI et Anthropic les chargent à la demande via leur propre recherche hébergée :

PHP
public function tools(): iterable
{
    return [
        new WeatherTool,
        new ToolSearch(tools: [new SearchInvoices, new RefundOrder]),
    ];
}

Les outils eux-mêmes ne changent pas : aucune interface à implémenter, aucune option de fournisseur. Le wrapper se traduit en {"type":"tool_search"} chez OpenAI et en types tool_search_tool_* chez Anthropic, dont la stratégie de recherche (regex ou bm25) est un argument de constructeur.

Les fournisseurs qui ne gèrent pas la recherche hébergée lèvent une exception claire avant l'envoi de la requête, plutôt que d'ignorer silencieusement les outils du wrapper. Un seul wrapper par requête, et la recherche hébergée OpenAI exige des réponses stockées : un ToolSearch utilisé avec store=false lève une exception.

Le point de mise à jour à ne pas rater

Une erreur de fournisseur rapportée dans le corps d'un stream — un HTTP 200 dont la charge SSE contient un objet error — lève désormais une StreamErrorException au lieu de terminer l'étape par un break. Auparavant, le consommateur récupérait un ->text partiel, aucune raison de fin, aucun StreamEnd terminal, et aucun signal que le run avait échoué. L'exception n'est délibérément pas failoverable, puisque ces erreurs arrivent avec un code 200.

À noter aussi : les applications qui utilisent le modèle texte par défaut de Gemini basculent sur gemini-3.7-flash.