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 :
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.