Ce qui se passe
Avec Laravel AI SDK 1.4.0, une exception levée dans un outil n'arrête plus l'agent. Avant, elle terminait l'exécution et déclenchait AgentFailed. Maintenant, le SDK l'attrape et renvoie son message au modèle comme un résultat d'outil en échec : le modèle peut expliquer le problème à l'utilisateur ou essayer un autre outil.
Exemple : un outil RefundOrder lève « Order 1234 can no longer be refunded. » Le modèle reçoit un échec d'appel qui porte ce texte, puis décide de la suite. Les outils approuvés qui levaient une exception étaient déjà traités ainsi ; tous les outils se comportent maintenant de la même manière (PR #1191, de Pascal Baljet).
Ce que votre application voit encore
- l'exception part toujours vers votre gestionnaire d'erreurs, via
report(); - l'événement
ToolFailedest toujours déclenché ; - une exécution en flux émet un événement
ToolResultavecsuccessfulàfalse.
Seule l'exception de l'outil est rattrapée : celle d'un écouteur d'événements fait toujours échouer l'exécution.
Pourquoi ça compte
Un cas métier prévu, comme une commande déjà remboursée, ne fait plus s'effondrer la conversation. Le revers est direct : le texte de l'exception est lu par le modèle, donc envoyé au fournisseur. Écrivez des messages qu'un modèle peut exploiter, sans détail interne ni secret.
Retrouver l'ancien comportement
Si du code dépend de l'arrêt de l'exécution, relancez l'exception depuis un écouteur de ToolFailed, puisque l'exception d'un écouteur, elle, interrompt l'exécution :
use Illuminate\Support\Facades\Event;
use Laravel\Ai\Events\ToolFailed;
Event::listen(ToolFailed::class, fn (ToolFailed $event) => throw $event->exception);Le reste de la 1.4
- Tests : sous
Agent::fake(), reprendre une approbation en pause exécute maintenant l'outil approuvé et enregistre son résultat (PR #1190). Un rejet sans motif termine le tour, comme chez un vrai fournisseur : un test qui attend une réponse doit passerDecision::reject('Not now.'). - Réponse brute : la propriété
raw, déjà sur les réponses texte, existe aussi sur celles de classification, d'embeddings, de transcription, d'image et de reranking (PR #1178 à #1182, de pushpak1300). Elle donne accès aux champs que le SDK ne mappe pas, par exempleusage.costchez OpenRouter. Elle vautnullaprès sérialisation, et sur des embeddings entièrement servis par le cache. - Facturation OpenAI : OpenAI répond 429 aussi bien pour une limite de débit que pour
insufficient_quotaoucredit_balance_exhausted. Les seconds lèvent maintenantInsufficientCreditsExceptionet ne sont plus rejoués ; avant, ils levaientRateLimitedExceptionet pouvaient être relancés siRetry-Afterétait court. Le failover ne change pas (PR #1174, de grzegorczykp). - Flux : une erreur de fournisseur est diffusée sous le nom
error, et non plus sous son code (403,PERMISSION_DENIED). Le champtypegarde le code ; un échec de job restestream_failed(PR #1208). - Titres de conversation : leur génération, qui fait un second appel au modèle, déclenche enfin les événements d'agent, avec
parentInvocationIdvers le tour d'origine (PR #1203). Vos écouteurs dePromptingAgent,AgentPromptedetAgentFailedles reçoivent aussi. - Correctifs : appels d'outils Anthropic (
end_turn,pause_turn), outils compatibles OpenAI sans index, ordre des embeddings OpenAI par lot, voix personnalisées, modèles Mistral.
Source : Laravel News, en anglais.