Agents
Agents
Ce plugin est actuellement en bêta. Les API sont susceptibles de changer d'une version mineure à l'autre. Importez depuis @databricks/appkit/beta. Consultez Niveaux de stabilité des plugins.
Le plugin agents transforme une AppKit app Databricks en hôte d'agents IA. Il détecte les définitions d'agents sur le disque — un dossier par agent sous server/agents/, contenant soit agent.md (markdown), soit agent.ts (code) — et les expose via POST /invocations et POST /responses (sans streaming, alias) ainsi que POST /chat (streaming) et des routes de gestion des threads, d'annulation et d'approbation HITL. Dans tous les cas, l'identifiant de l'agent correspond au nom de son dossier : aucune table de correspondance à maintenir, aucun identifiant à redéclarer.
Cette page couvre l'ensemble du cycle de vie. Pour les primitives écrites à la main (tool(), mcpServer()), voir tools.
Prérequis
Le plugin agents pilote le LLM via des Server-Sent Events. Les Foundation Model APIs (Claude, Llama, GPT, etc.) et les autres endpoints de type chat prennent en charge le streaming et fonctionnent sans configuration supplémentaire. Les endpoints de modèles personnalisés qui renvoient une réponse JSON unique (par exemple les déploiements sklearn ou MLflow pyfunc classiques) ne font pas de streaming — pointer un agent vers l'un d'eux provoquera l'échec du premier tour avec le message « Response body is null — streaming not supported ». Si vous indiquez un serving endpoint dans apps init, choisissez-en un dont le modèle implémente le protocole de streaming chat-completions ; le plugin agents lit son nom depuis DATABRICKS_SERVING_ENDPOINT_NAME dès qu'un agent ne définit pas lui-même model:.
Pour un appel sans streaming vers un endpoint personnalisé, utilisez plutôt la route /invoke du plugin serving avec useServingInvoke.
Vous pouvez aussi faire l'impasse sur la configuration d'un serving endpoint grâce à l'adaptateur Supervisor API managé (bêta).
Installation
agents est un plugin comme les autres. Ajoutez-le à plugins[], aux côtés de server() et de tous les plugins ToolProvider dont vous voulez rendre les outils accessibles aux agents.
import { analytics, createApp, files, server } from "@databricks/appkit";
import { agents } from "@databricks/appkit/beta";
await createApp({
plugins: [server(), analytics(), files(), agents()],
});Cela suffit à obtenir un serveur HTTP opérationnel exposant POST /invocations (et son alias POST /responses), relié à un agent piloté par markdown. Utilisez plutôt POST /chat si vous souhaitez une interface en streaming et compatible HITL.
Niveau 1 : déposer un package d'agent markdown
Chaque agent réside dans son propre dossier sous server/agents/, avec le fichier d'entrée agent.md. Un dossier ne constitue un agent que s'il contient un fichier d'entrée (agent.md ou agent.ts) ; à défaut, il est ignoré. Les dossiers de ressources propres à chaque agent se placent donc à côté du fichier d'entrée — notamment un dossier skills/ contenant les Compétences (des ensembles d'instructions à la demande que l'agent charge par leur nom). Un dossier partagé server/agents/skills/ regroupe les compétences accessibles à tous les agents.
my-app/
server/
server.ts
agents/
assistant/
agent.md---
endpoint: databricks-claude-sonnet-4-5
default: true
---
You are a helpful data assistant running on Databricks.
Use the available tools to query data, browse files, and help users.Au démarrage, le plugin :
- Découvre
server/agents/assistant/agent.mdet enregistre l'id d'agentassistant. - Analyse le frontmatter YAML et le corps markdown comme
instructionsde l'agent. - Résout l'adaptateur à partir de
endpoint(ou se rabat surDATABRICKS_SERVING_ENDPOINT_NAME). - Monte l'agent sous le nom par défaut (
assistant).
L'agent démarre sans aucun outil. Les outils sont opt-in : déclarez-les dans le frontmatter (niveau 2 ci-dessous) ou activez explicitement l'héritage automatique avec agents({ autoInheritTools: { file: true } }). Voir « Posture d'héritage automatique » plus bas pour savoir ce que cela coûte et pourquoi c'est désactivé par défaut.
Les versions antérieures plaçaient les agents markdown sous config/agents/<id>/agent.md. Cet emplacement est toujours lu à titre de repli déprécié (avertissement unique au démarrage) ; déplacez chaque dossier vers server/agents/<id>/agent.md afin que tous les agents — markdown comme code — soient regroupés au même endroit.
Les requêtes arrivent sur POST /invocations (ou son alias POST /responses) avec un corps compatible OpenAI Responses. Ces endpoints exécutent l'agent jusqu'à son terme et renvoient une réponse JSON unique — sans SSE. Les clients en streaming doivent utiliser POST /chat. Chaque appel d'outil est tracé automatiquement. Les appels d'outils du toolkit de plugin (les entrées plugin:<name> / plugins.<name>.toolkit()) passent en plus par asUser(req) : leur SQL s'exécute donc sous l'identité de l'utilisateur à l'origine de la requête et l'accès aux fichiers respecte les ACL Unity Catalog. Un tool({ execute }) écrit à la main n'est pas encapsulé : son execute ne reçoit que les arguments d'outil validés (pas de req), il s'exécute donc sous l'identité du principal de service de l'application et ne peut pas basculer en OBO. Si un outil doit agir au nom de l'utilisateur à l'origine de la requête, exposez-le comme outil de plugin plutôt que via un execute écrit à la main. Voir Contexte d'exécution.
La surface d'invocation non-streaming n'offre aucun moyen de remonter à l'appelant une demande d'approbation en cours d'appel. Lorsque approval.requireForDestructive est activé (par défaut) et que l'agent résolu comporte un outil annoté avec un effet mutant (effect: "write" | "update" | "destructive", ou l'ancien destructive: true), POST /invocations et POST /responses rejettent la requête avec un HTTP 400 avant l'exécution de l'adaptateur. Basculez les agents compatibles HITL vers POST /chat, ou désactivez l'approbation via agents({ approval: { requireForDestructive: false } }) pour les agents autonomes de back-office.
Niveau 2 : délimiter la portée des outils dans le frontmatter
---
endpoint: databricks-claude-sonnet-4-5
tools:
- plugin:analytics # tous les outils analytics.*
- plugin:files: [uploads.read, uploads.list] # uniquement ces outils de files
- plugin:genie: { except: [getConversation] } # tout sauf getConversation
- get_weather # outil ambiant déclaré dans le code
default: true
---
You are a read-only data analyst.La liste unifiée tools: combine des références de plugins et des outils ambiants, à l'image de la forme fonctionnelle TS tools(plugins) => ({ ...plugins.analytics.toolkit(), ...plugins.files.toolkit({ only: [...] }), get_weather: tool({...}) }). Chaque entrée prend l'une des formes suivantes :
plugin:<name>— récupère tous les outils du plugin indiqué.plugin:<name>: [tool1, tool2]— uniquement les outils listés (raccourci pour{ only: [...] }).plugin:<name>: { ...ToolkitOptions }— options complètesprefix/only/except/rename.<key>(sans préfixe) — nom d'outil ambiant résolu à partir de la configurationagents({ tools: { ... } }).
Dès qu'un tools: est déclaré, l'héritage automatique par défaut est désactivé : l'agent ne voit que les outils listés.
Niveau 3 : agents définis par code
Les agents définis par code se placent à raison d'un par dossier sous server/agents/, avec pour fichier d'entrée agent.ts (à l'image du fichier agent.md en markdown). Ce point d'entrée exporte un agent créé et son id correspond au nom du dossier (server/agents/support/agent.ts → support). Nulle part l'id n'est répété.
// server/agents/support/agent.ts
import { createAgent, tool } from "@databricks/appkit/beta";
import { z } from "zod";
export default createAgent({ // id dérivé du nom du dossier : "support"
instructions: "You help customers with data and files.",
model: "databricks-claude-sonnet-4-5", // sucre syntaxique sous forme de chaîne
tools(plugins) {
return {
...plugins.analytics.toolkit(), // tous les outils analytics
...plugins.files.toolkit({ only: ["uploads.read"] }), // sous-ensemble filtré
get_weather: tool({
description: "Weather",
schema: z.object({ city: z.string() }),
execute: async ({ city }) => `Sunny in ${city}`,
}),
};
},
});Le plugin agents détecte ces fichiers au démarrage — aucun enregistrement, aucune table de correspondance :
// server/server.ts
import { analytics, createApp, files, server } from "@databricks/appkit";
import { agents } from "@databricks/appkit/beta";
await createApp({
plugins: [server(), analytics(), files(), agents()], // pas de table d'agents, pas d'import
});La découverte importe chaque server/agents/<id>/agent.ts — la source .ts via tsx en dev, et le fichier compilé dist/agents/<id>/agent.js dans un build de production (la sortie compilée l'emporte sur la source, indépendamment de NODE_ENV). Comme le serveur de production est bundlé et n'importe que ce qui est accessible depuis server/server.ts, la configuration tsdown du modèle déclare server/agents/*/agent.ts comme points d'entrée de build afin que les fichiers dist/agents/*/agent.js soient générés pour le scan — c'est ce câblage qui permet à un dossier simplement déposé de survivre au bundle de prod. (Le markdown agent.md, lui, est lu depuis la source aussi bien en dev qu'en prod — ce sont des données, pas du code compilé.) La racine est toujours server/agents — aucune option de configuration ne permet de la déplacer ; le markdown encore situé sous config/agents/ est lu comme solution de repli dépréciée (avertissement unique).
Comme la sortie compilée l'emporte sur la source, un dist/agents / build/agents obsolète issu d'un précédent npm run build sera pris en compte par npm run dev à la place de vos server/agents/*.ts en direct, donnant l'impression que vos modifications sont ignorées. Supprimez le répertoire de build si un agent défini par code semble figé — une reconstruction ne fait que remplacer l'instantané par un plus récent ; seule la suppression rétablit le rechargement à chaud depuis la source en dev. Le markdown est toujours lu depuis la source : les modifications de agent.md ne sont donc jamais masquées.
Le point d'entrée peut faire export default createAgent({...}) ou exporter un unique agent créé et nommé ; dans les deux cas, l'id correspond au nom du dossier. Un dossier dont le point d'entrée n'exporte aucun agent créé (ou qui n'a ni agent.ts ni agent.md) est ignoré. Désignez un agent comme agent par défaut avec createAgent({ default: true }) (équivalent du frontmatter markdown default: true) ; un agents({ defaultAgent }) explicite reste prioritaire.
Les agents définis par code démarrent sans aucun outil par défaut. La forme fonction tools(plugins) => Record<string, AgentTool> est le principal moyen d'intégrer les outils des plugins : chaque plugin enregistré dans createApp({ plugins: [...] }) apparaît sur le paramètre plugins, et il suffit d'appeler .toolkit(opts?) dessus pour obtenir un enregistrement facile à décomposer. Le runtime invoque la fonction une seule fois lors de la configuration de l'agent et met le résultat en cache — chaque plugin n'est mentionné qu'une seule fois (dans createApp), sans variables conservées ni imports marqueurs.
Les appels tool({...}) en ligne figurent dans le même enregistrement. Leur name est facultatif — le plugin agents le remplace par la clé de l'enregistrement (get_weather ci-dessus).
L'héritage automatique est désactivé par défaut pour les deux origines — un agent markdown ou en code sans tools: déclaré obtient un index d'outils vide. Activez explicitement une origine avec agents({ autoInheritTools: { file: true } }) (ou { code: true }, ou true pour les deux).
Passer une map d'agents construite à la main fonctionne toujours et reste prise en charge pour des raisons de rétrocompatibilité, mais cela déclenche un avertissement de dépréciation unique et sera supprimé dans une future version mineure. Cette approche oblige à répéter l'id de chaque agent (une fois dans createAgent, une fois comme clé de la map) ; la découverte depuis server/agents/ supprime à la fois la map et cette répétition. Pour migrer, déplacez chaque createAgent(...) dans son propre fichier server/agents/<id>/agent.ts (export par défaut ou export nommé unique), puis supprimez la map. Si un agent découvert et une entrée de la map partagent un id, la découverte l'emporte et l'entrée de la map est ignorée (avec un avertissement unique). (Les sous-agents en ligne — createAgent({ agents: { ... } }) sur une définition — ne sont pas concernés ; seule la map au niveau du plugin est dépréciée.)
Certains exemples plus bas passent encore les agents en ligne via cette map par souci de concision — dans une application réelle, chacune de ces définitions createAgent(...) réside dans son propre fichier server/agents/<id>/agent.ts et ne nécessite aucune map.
Restreindre les outils dans le code
plugins.<name>.toolkit(opts?) accepte les mêmes ToolkitOptions que le frontmatter markdown :
| Option | Exemple | Signification |
|---|---|---|
only | { only: ["query"] } | Liste blanche de noms d'outils locaux |
except | { except: ["legacy"] } | Liste noire de noms d'outils locaux |
prefix | { prefix: "" } | Supprime le préfixe ${pluginName}. |
rename | { rename: { query: "q" } } | Renomme des noms locaux spécifiques |
Pour les plugins qui n'exposent pas de méthode .toolkit() (par exemple des plugins ToolProvider tiers écrits avec un simple toPlugin), le runtime se rabat sur un parcours de getAgentTools() et génère des clés avec espace de noms (${pluginName}.${localName}). Ce mécanisme de repli respecte only / except / rename / prefix de la même manière.
Si un plugin référencé n'est pas enregistré dans createApp({ plugins }), le plugin agents lève une erreur lors de la configuration, accompagnée d'une liste Available: …, ce qui vous permet de corriger le câblage avant la première requête.
Niveau 4 : sous-agents
const researcher = createAgent({
instructions: "Research the question. Return concise bullets.",
model: "databricks-claude-sonnet-4-5",
tools: { search: tool({ /* ... */ }) },
});
const writer = createAgent({
instructions: "Draft prose from notes.",
model: "databricks-claude-sonnet-4-5",
});
const supervisor = createAgent({
instructions: "Coordinate researcher and writer.",
model: "databricks-claude-sonnet-4-5",
agents: { researcher, writer }, // exposés sous les noms agent-researcher, agent-writer
});
// server/agents/{supervisor,researcher,writer}/agent.ts — un dossier chacun
export default supervisor;
await createApp({
plugins: [server(), agents()], // découverts depuis server/agents/
});Placez supervisor, researcher et writer dans leurs propres dossiers server/agents/<id>/agent.ts (chacun avec un export par défaut) — un parent en markdown peut lui aussi déléguer à un enfant en code situé dans un dossier voisin via le frontmatter agents: [helper]. Chaque clé de agents: {...} dans une AgentDefinition devient un outil agent-<key> sur le parent. À l'appel, le plugin agents exécute l'adaptateur de l'enfant avec une liste de messages vierge (aucun état de thread partagé) et renvoie le texte agrégé. Les cycles dans le graphe agents: {} intégré d'un agent en code sont rejetés au chargement (createAgent) ; la délégation agents: en markdown rejette les auto-références au chargement et borne les cycles plus profonds au runtime via limits.maxSubAgentDepth.
Compétences
Les compétences sont des lots d'instructions chargés à la demande — au même format SKILL.md que celui utilisé par Claude Code et Cursor. Seuls les champs name et description de chaque compétence figurent dans le prompt système (toujours actifs, peu coûteux) ; le corps complet n'est chargé qu'à la demande, lorsque l'agent (ou l'utilisateur) l'invoque. Cela fonctionne avec n'importe quel modèle servi par Databricks — AppKit implémente lui-même ce chargement progressif, sans dépendre d'une fonctionnalité de compétences propre au fournisseur.
Une compétence est un répertoire contenant un fichier SKILL.md ainsi que tous les fichiers de référence associés :
server/agents/
skills/ # pool partagé — tout agent peut s'y inscrire
pdf-forms/
SKILL.md
reference.md
planner/
agent.md
skills/ # réservé à l'agent `planner`
house-style/
SKILL.md---
name: pdf-forms
description: Fill and validate PDF form fields from a data record.
---
To fill a PDF form:
1. Read `reference.md` for the field-name conventions.
2. ...name et description sont obligatoires ; license, allowed-tools et metadata sont acceptés pour assurer la compatibilité avec les compétences créées ailleurs. Les clés inconnues génèrent un avertissement et sont ignorées.
Visibilité
- Les compétences propres à un agent (
server/agents/<id>/skills/) sont toujours visibles par cet agent. - Les compétences globales (
server/agents/skills/et les compétences stockées dans un volume de catalogue) sont opt-in : déclarez-les dans le frontmatter de l'agent,skills: [pdf-forms]. DéfinissezautoInheritSkills: true(ou{ file, code }) sur le plugin pour rendre visible chaque compétence globale sans avoir à les lister — désactivé par défaut, afin que le catalogue toujours actif de chaque agent reste léger.
Comment l'agent utilise une compétence
Deux outils intégrés en lecture seule sont injectés dans tout agent disposant d'un catalogue visible :
load_skill(skill)— renvoie les instructions complètes de la compétence ainsi qu'un manifeste des fichiers qui lui sont associés.read_skill_file(skill, path)— renvoie le contenu de l'un de ces fichiers.
Le modèle appelle load_skill de lui-même lorsqu'une tâche correspond à la description d'une compétence. Un utilisateur peut imposer une compétence précise pour un tour de conversation grâce au préfixe /skill-name dans le chat (ou à l'option send(message, { skill }) de useAgentChat) ; les instructions de la compétence sont alors injectées de manière déterministe dans ce tour, et load_skill reste disponible pour la sélection automatique. Le client lit le catalogue propre à chaque agent depuis la charge utile clientConfig() du plugin pour alimenter un sélecteur.
Compétences du catalogue (volume Unity Catalog)
Faites pointer skillsVolume (ou la variable d'environnement DATABRICKS_VOLUME_AGENT_SKILLS) vers un volume UC organisé de la même façon — <volume>/<name>/SKILL.md. Les compétences du catalogue sont détectées au démarrage et lors d'un reload(), fusionnées dans le pool global partagé, puis lues sous l'identité du service principal (skillCredentialMode vaut "sp" par défaut). Elles sont pensées comme un pool partagé et organisé ; les volumes de compétences par utilisateur (OBO) ne sont pas encore pris en charge. Déclarer la ressource facultative volume dans le manifeste permet au générateur d'ossature d'accorder l'accès en lecture au SP.
Collisions de noms
Les noms de compétences sont référencés tels quels. Si deux sources fournissent le même nom, chacune devient un <scope>:name qualifié (agent:, bundle:, volume:) et le nom simple est rejeté comme ambigu, avec la liste des alternatives possibles. En revanche, deux compétences portant le même nom au sein d'une même source provoquent une erreur au démarrage.
Limites de la v1
- Les scripts ne sont pas exécutés. Une compétence peut référencer
scripts/foo.py; la v1 ne charge que le texte et les documents de référence. allowed-toolsest purement indicatif. Ce champ apparaît comme une simple indication dans la compétence chargée, sans être appliqué — charger une compétence ne restreint pas les outils que l'agent peut appeler. Il ne s'agit pas d'un bac à sable.- Le contenu des compétences ne fait pas l'objet d'un contrôle d'accès par utilisateur (il est lu en tant que SP). N'y placez aucun contenu sensible propre à un utilisateur.
Niveau 5 : autonome (sans createApp)
import { createAgent, runAgent, tool } from "@databricks/appkit/beta";
import { z } from "zod";
const classifier = createAgent({
instructions: "Classify tickets: billing | bug | feature.",
model: "databricks-claude-sonnet-4-5",
tools: {
lookup_account: tool({ /* ... */ }),
},
});
for (const ticket of tickets) {
const result = await runAgent(classifier, {
messages: [{ role: "user", content: ticket.body }],
});
await persistClassification(ticket.id, result.text);
}runAgent pilote l'adaptateur sans createApp ni HTTP. Les appels tool() en ligne fonctionnent de façon autonome, comme illustré ci-dessus. Pour utiliser les outils de plugins en mode autonome, passez les fabriques de plugins via RunAgentInput.plugins et accédez-y grâce à la forme fonction tools(plugins) :
import { analytics } from "@databricks/appkit";
import { createAgent, runAgent } from "@databricks/appkit/beta";
const classifier = createAgent({
instructions: "Classify tickets. Use analytics.query for historical data.",
model: "databricks-claude-sonnet-4-5",
tools(plugins) {
return { ...plugins.analytics.toolkit() };
},
});
const result = await runAgent(classifier, {
messages: "is ticket 42 a duplicate?",
plugins: [analytics()],
});runAgent construit immédiatement chaque plugin de RunAgentInput.plugins, exécute le cycle de vie standard attachContext({}) + await setup(), et partage les instances entre le run de premier niveau et chaque dispatch de sous-agent. Les plugins dont le setup() nécessite un runtime réservé à createApp (par exemple WorkspaceClient, ServiceContext) échouent dès l'initialisation en mode autonome avec un message explicite "use createApp instead", plutôt qu'en cours de traitement.
Les outils hébergés MCP (mcpServer(...)) requièrent toujours agents() (ils ont besoin d'un client MCP actif). En revanche, les outils hébergés de l'API Supervisor (supervisorTools.*) fonctionnent avec runAgent en mode autonome — l'adaptateur dispose de tout le nécessaire pour les exécuter côté serveur. Il devient ainsi possible d'utiliser les Supervisor Agents en évaluation par lots ou en CI sans createApp. En mode autonome, le dispatch des outils de plugin s'exécute en tant que service principal (sans OBO) et contourne la barrière d'approbation du plugin agents : considérez runAgent en mode autonome comme un environnement à prompts de confiance (CI, évaluation par lots, scripts internes), et non comme une surface exposée aux utilisateurs.
Ajouter des agents à une application existante
Vous avez déjà une application et souhaitez y ajouter des agents ? Ce que vous devez modifier dépend du type :
Agents markdown — uniquement le plugin. Déposez server/agents/<id>/agent.md, ajoutez agents() à vos plugins, et c'est tout. Le markdown est lu depuis les sources au runtime, aussi bien en dev qu'en prod : aucune modification du build n'est nécessaire.
Agents définis par code (server/agents/<id>/agent.ts) — mettez également à jour le build de votre serveur pour qu'un bundle de production les génère. Les agents définis par code ne sont importés nulle part : un build qui compile uniquement server/server.ts ne produira jamais dist/agents/*/agent.js, et un npm run build bundlé suivi d'un démarrage ne découvrirait aucun agent défini par code.
npm run dev (tsx) importe directement les sources .ts : les agents définis par code y fonctionnent donc sans modification du build — l'écart n'apparaît que dans un build bundlé. Si vous ajoutez des agents définis par code sans adapter le build, le plugin émet un avertissement au démarrage (en indiquant le correctif) plutôt que d'échouer silencieusement.
Le correctif tient en une ligne : adoptez le préréglage de build.
// tsdown.server.config.ts
import { appkitServerConfig } from '@databricks/appkit/tsdown';
export default appkitServerConfig();appkitServerConfig() détecte automatiquement server/agents/ et n'ajoute le glob d'entrée + clean que si des agents définis par code sont présents ; passez vos surcharges via appkitServerConfig({ external, define, ... }), ou une fonction appkitServerConfig((base) => ({ ...base })) pour un contrôle total. C'est aussi la dernière fois que vous touchez à ce fichier : les futures évolutions de la configuration de build sont livrées avec le package. Si vous préférez conserver une configuration écrite à la main, ajoutez les entrées vous-même :
entry: ['server/server.ts', 'server/agents/*/agent.ts'],
clean: true,Agents gérés : l'adaptateur Supervisor API
DatabricksAdapter.fromSupervisorApi (bêta) est le moyen le plus simple d'exécuter un agent, sans aucune configuration : au lieu de provisionner un serving endpoint puis de le cibler, vous exécutez la boucle agentic directement dans le workspace Databricks en ciblant l'API Responses de l'AI Gateway (/ai-gateway/mlflow/v1/responses), qui exécute le LLM — ainsi que les éventuels outils hébergés — sous forme de service géré sur Databricks. Pas de DATABRICKS_SERVING_ENDPOINT_NAME, pas de vérification de la prise en charge du streaming, pas de tuyauterie d'outils JS pour les cas courants.
L'agent minimal ne demande qu'une ligne de plus qu'un agent markdown :
import { createApp } from "@databricks/appkit";
import { agents, createAgent, DatabricksAdapter } from "@databricks/appkit/beta";
await createApp({
plugins: [
agents({
agents: {
assistant: createAgent({
instructions: "You are a helpful assistant.",
model: DatabricksAdapter.fromSupervisorApi({
model: "databricks-claude-sonnet-4-5",
}),
}),
},
}),
],
});createAgent({ model }) accepte déjà les adaptateurs et les promesses d'adaptateurs, en plus de la chaîne de caractères du nom de modèle utilisée dans les exemples précédents : vous pouvez donc lui transmettre directement le résultat de la fabrique. Celle-ci résout les identifiants via la chaîne du SDK (DATABRICKS_HOST, OAuth, PAT, etc.) ; passez workspaceClient pour réutiliser un client existant.
Outils hébergés
Exposez au modèle des spaces Genie, des fonctions/connexions Unity Catalog, des Knowledge Assistants ou d'autres AppKit apps en les déclarant comme outils d'agent — au même endroit que tous les autres outils. L'exécution reste côté serveur : vous n'écrivez aucun code d'outil :
import {
createAgent,
DatabricksAdapter,
supervisorTools,
} from "@databricks/appkit/beta";
const assistant = createAgent({
instructions: "You are a helpful data assistant.",
model: DatabricksAdapter.fromSupervisorApi({
model: "databricks-claude-sonnet-4-5",
}),
tools: () => ({
nyc: supervisorTools.genieSpace({
id: "01ABCDEF12345678",
description: "NYC taxi trip records and zones",
}),
add: supervisorTools.ucFunction({
name: "main.default.add",
description: "Adds two integers and returns the sum.",
}),
}),
});Chaque fabrique supervisorTools.* prend un unique objet d'options nommées — les chaînes critiques pour le routage sont étiquetées sur le site d'appel, ce qui rend impossibles les erreurs d'inversion d'arguments positionnels.
description est obligatoire et non vide — le LLM s'en sert pour router entre les outils : deux Genie spaces portant tous deux l'étiquette "Genie space" seront donc impossibles à distinguer.
La description d'un outil hébergé est lue par le LLM pour décider quand router vers cet outil. Ne la dérivez pas d'une entrée non fiable — messages utilisateur, corps de requête, champs libres provenant de systèmes externes, ou toute valeur qu'un attaquant pourrait influencer. Traitez description (ainsi que id/name) comme contrôlée par l'application, au même titre que les instructions de l'agent. Autoriser ici une chaîne contrôlée par l'utilisateur crée un point d'injection de prompt : une description malveillante peut amener le modèle à router vers un outil (ou à l'écarter) pour toute requête future traitée par l'agent.
La même prudence s'applique aux description MCP et à tout autre champ lu par le modèle au moment du routage.
| Fabrique | Type d'outil | Identifiant |
|---|---|---|
supervisorTools.genieSpace({ id, description }) | Genie space | id de la space |
supervisorTools.ucFunction({ name, description }) | Fonction Unity Catalog | nom en trois parties |
supervisorTools.knowledgeAssistant({ knowledgeAssistantId, description }) | Knowledge Assistant | id de l'assistant |
supervisorTools.app({ name, description }) | Databricks App | nom de l'app |
supervisorTools.ucConnection({ name, description }) | Connexion UC | nom de la connexion |
Déclarer des outils hébergés dans les agents markdown
Les outils du superviseur hébergé fonctionnent également dans les agents pilotés par markdown : déclarez l'outil dans le code (sous agents({ tools: { ... } })) et référencez sa clé dans le frontmatter :
// server.ts
agents({
agents: { /* ... */ },
tools: {
nyc_taxi: supervisorTools.genieSpace({
id: "01ABCDEF12345678",
description: "NYC taxi trip records and zones",
}),
},
});---
endpoint: databricks-claude-sonnet-4-5
tools:
- nyc_taxi
---
You answer questions about NYC taxi data using the Genie space.Aucune nouvelle syntaxe de frontmatter — la résolution des outils ambiants dans tools: fait déjà correspondre les clés simples à agents({ tools }), et la structure d'enregistrement typé de supervisorTools.* permet au plugin de les classer automatiquement.
Ce qui ne s'applique pas aux agents Supervisor-API
Le runtime managé gère lui-même l'exécution de ses outils : l'adaptateur ignore donc volontairement les outils de fonction et les sous-agents issus de l'index d'outils du plugin agents. Pour tout agent dont le model: est un adaptateur Supervisor :
- Seules les entrées
supervisorTools.*parviennent au modèle. Les outils de fonction (tool({...})), les outils hébergés MCP (mcpServer(...)) et les sous-agents locaux (agents: { ... }) déclarés aux côtés d'un adaptateur Supervisor déclenchent un avertissement au moment de l'enregistrement et ne sont pas exposés au modèle. La vérification de capacité découle deconsumesInputTools: falsesur l'adaptateur. - Le contrôle d’approbation humaine (human-in-the-loop) ne se déclenche pas (les appels d'outils n'entrent jamais dans le processus Node ; les annotations
effect: "destructive"n'ont aucun effet sur les outils hébergés). limits.maxToolCallsn'est pas appliqué (le runtime managé comptabilise lui-même ses appels).- L'OBO par appel ne s'applique pas aux outils hébergés : ceux-ci s'exécutent avec les identifiants utilisés par le runtime managé pour la ressource cible.
Composition de sous-agents entre adaptateurs
Les adaptateurs supervisor et chat-completions peuvent tous deux figurer dans la même table agents({ agents: { ... } }), mais la composition ne fonctionne que dans un sens :
- Parent chat-completions → sous-agent supervisor fonctionne nativement. Le parent effectue la répartition via
agent-{key}comme un outil de fonction ordinaire ; l'adaptateur de l'enfant s'exécute entièrement sur l'AI Gateway. - Parent supervisor → enfants outil de fonction / sous-agent local n'est pas encore pris en charge. La vérification des capacités émet un avertissement à l'enregistrement ; ces outils n'atteindront pas le modèle supervisor. Cette restriction sera levée ultérieurement en routant les événements
response.function_callde SA viacontext.executeTool.
Certains types d'outils hébergés renvoient leur texte d'assistant final sans événements output_text.delta incrémentaux. L'adaptateur dispose d'une voie de récupération qui extrait le texte de response.completed.output[] afin que le tour ne soit pas vide sans explication. Définissez DEBUG=appkit:agents:supervisor-api pour consigner l'histogramme des types d'événements par tour et vérifier ainsi quelle voie a été empruntée.
Référence de configuration
agents({
// Les agents résident sous server/agents/<id>/ (racine fixe). config/agents est lu en solution de repli, désormais obsolète.
agents?: Record<string, AgentDefinition>, // OBSOLÈTE — utilisez la découverte via server/agents/<id>/
defaultAgent?: string,
defaultModel?: AgentAdapter | Promise<AgentAdapter> | string,
tools?: Record<string, AgentTool>,
autoInheritTools?: boolean | { file?: boolean, code?: boolean },
autoInheritSkills?: boolean | { file?: boolean, code?: boolean }, // désactivé par défaut
skillsVolume?: string, // Volume UC pour les compétences du catalogue ; repli sur DATABRICKS_VOLUME_AGENT_SKILLS
skillCredentialMode?: "sp" | "obo", // "sp" par défaut (voir Compétences)
threadStore?: ThreadStore, // en mémoire par défaut
baseSystemPrompt?: false | string | (ctx: PromptContext) => string,
mcp?: {
trustedHosts?: string[], // noms d'hôtes supplémentaires autorisés pour les URL MCP personnalisées
allowLocalhost?: boolean, // par défaut : NODE_ENV !== "production"
},
approval?: {
requireForDestructive?: boolean, // par défaut : true
timeoutMs?: number, // par défaut : 60_000
},
limits?: {
maxConcurrentStreamsPerUser?: number, // par défaut : 5
maxToolCalls?: number, // par défaut : 50
maxSubAgentDepth?: number, // par défaut : 3
toolCallTimeoutMs?: number, // par défaut : 300_000 (5 min)
},
})Par défaut, autoInheritTools vaut { file: false, code: false } — aucun outil n'est propagé vers un agent, sauf si le développeur l'active explicitement. Lorsque l'option est activée, seuls les outils marqués autoInheritable: true par l'auteur du plugin sont propagés ; les outils destructifs ou modifiant l'état restent toujours exclus du mécanisme d'héritage automatique, même lorsque l'option est activée. La forme booléenne abrégée (autoInheritTools: true) s'applique aux deux origines. Voir « Posture d'héritage automatique » ci-dessous.
Politique d'hôtes MCP
AppKit applique une politique de zéro confiance à chaque URL MCP utilisée comme outil hébergé. Par défaut, seules les URL de workspace Databricks de même origine (correspondant au DATABRICKS_HOST résolu) sont accessibles. Tout autre hôte doit être explicitement ajouté à la liste d'autorisation via mcp.trustedHosts, et les identifiants du workspace (jetons de principal de service et jetons utilisateur on-behalf-of) ne sont jamais transmis à ces hôtes.
agents({
agents: {
support: createAgent({
instructions: "…",
tools: {
"mcp.internal": mcpServer("internal", "https://mcp.corp.internal/mcp"),
},
}),
},
mcp: {
trustedHosts: ["mcp.corp.internal"],
},
});La politique applique quatre règles au moment du connect() MCP, avant l'envoi du moindre octet :
- Seules les URL
httpethttpssont acceptées. - Le
http://en clair est rejeté partout, sauf pourlocalhostlorsqueallowLocalhostvaut true (activé par défaut en développement, désactivé en production). - Le nom d'hôte de destination doit correspondre à l'hôte du workspace, être égal à
localhost(si autorisé) ou figurer danstrustedHosts. - L'adresse DNS résolue ne doit pas appartenir aux plages loopback, RFC1918, CGNAT (100.64.0.0/10), link-local (169.254.0.0/16 — qui couvre les services de métadonnées cloud), ULA ou multicast.
Les en-têtes Authorization transportant des identifiants du workspace sont limités aux URL du workspace de même origine. Un mcpServer(name, url) pointant vers un hôte externe de confiance doit s'authentifier lui-même (par exemple, au moyen d'un jeton personnalisé intégré à url).
Approche de l'héritage automatique
AppKit considère l'héritage automatique comme une opération à double clé : le développeur doit activer autoInheritTools, ET l'auteur du plugin doit marquer chaque outil comme autoInheritable: true. Ces deux conditions sont indispensables pour qu'un outil soit propagé dans l'index d'un agent sans configuration explicite.
// Activation au niveau du plugin agents (choisissez-en une) :
agents({ autoInheritTools: true }); // les deux origines
agents({ autoInheritTools: { file: true } }); // agents markdown uniquement
agents({ autoInheritTools: { file: true, code: true } });
// Outil par outil, au sein d'un plugin :
defineTool({
description: "safe read",
schema: z.object({ ... }),
annotations: { effect: "read", requiresUserContext: true },
autoInheritable: true, // consentement explicite : cet outil peut se propager automatiquement
execute: (args, signal) => ...,
});Les plugins principaux d'AppKit sont livrés avec les marquages autoInheritable suivants :
| Outil | autoInheritable | Justification |
|---|---|---|
analytics.query | oui | SQL en lecture seule, limité par OBO et appliqué au runtime via le classificateur |
files.list / files.read / files.exists / files.metadata | oui | Opérations de lecture limitées par OBO |
files.upload / files.delete | non | Modifient l'état — à câbler explicitement |
genie.getConversation | oui | Historique en lecture seule |
genie.sendMessage | non | Conversation Genie modifiant l'état |
lakebase.query | non | Déjà contrôlé par exposeAsAgentTool ; l'héritage automatique reste fermé, par défense en profondeur |
Les plugins ToolProvider tiers qui n'exposent pas de méthode toolkit() sont eux aussi exclus du chemin d'héritage automatique : leurs outils doivent être câblés explicitement via tools:. À la configuration, le plugin agents journalise ce que chaque agent a hérité et ce qui a été ignoré, afin de rendre cette posture visible :
[agents] [agent support] auto-inherited 2 tool(s): analytics.query, files.uploads.read
[agents] [agent support] auto-inherit skipped 3 tool(s) not marked autoInheritable: files(2), genie(1). Wire them explicitly via `tools:` if needed.Outils SQL pour agents
Deux outils d'agent intégrés peuvent exécuter du SQL pour le compte du LLM : analytics.query (sur le SQL warehouse Databricks) et lakebase.query, à activer explicitement (sur une base de données Lakebase Postgres). Tous deux obéissent à des règles de sécurité distinctes, car ils s'exécutent avec des privilèges différents.
analytics.query s'exécute avec le jeton OBO de l'appelant (les identifiants Databricks de l'utilisateur final). Son annotation readOnly: true est appliquée au moment de l'exécution : les instructions sont tokenisées et seuls SELECT, WITH, SHOW, EXPLAIN, DESCRIBE et DESC sont acceptés. Les écritures, le DDL et les instructions chaînées sont rejetés avant que la requête n'atteigne le warehouse :
// accepté
analytics.query({ query: "SELECT * FROM main.sales.orders WHERE created_at > current_date() - 7" })
// rejeté au niveau du plugin, n'atteint jamais le warehouse
analytics.query({ query: "UPDATE main.sales.orders SET status = 'cancelled'" })
analytics.query({ query: "SELECT 1; DROP TABLE main.sales.orders" })lakebase.query n'est pas enregistré comme outil d'agent par défaut. Son activation est une décision explicite, car le pool Lakebase est lié au service principal de l'application : un agent ayant accès à cet outil peut exécuter du SQL sous l'identité du SP, quel que soit l'utilisateur final à l'origine de la requête. Activez-le à l'aide d'un indicateur de prise de connaissance :
lakebase({
exposeAsAgentTool: {
iUnderstandRunsAsServicePrincipal: true,
readOnly: true, // par défaut
},
});Avec readOnly: true (valeur par défaut), le même classificateur SQL que pour analytics.query s'applique, et l'instruction acceptée est de plus encapsulée dans BEGIN READ ONLY; … ROLLBACK;, afin que le serveur Postgres rejette toute écriture ayant échappé au classificateur (par exemple un SELECT portant sur une fonction à effets de bord). L'annotation de l'outil est { effect: "read" }.
Avec readOnly: false, l'outil accepte n'importe quelle requête SQL et porte l'annotation { effect: "destructive" }. L'effet destructive déclenche le contrôle d'approbation humaine (human-in-the-loop, voir ci-dessous) à chaque invocation.
Approbation humaine (human-in-the-loop) pour les outils mutateurs
Tout outil annoté avec un effet mutateur — effect: "write" | "update" | "destructive" (recommandé) ou le booléen hérité destructive: true — nécessite une approbation explicite de l'utilisateur avant son exécution. Sécurisé par défaut : ne définissez approval.requireForDestructive: false que pour des agents back-office entièrement autonomes s'exécutant dans des contextes mono-utilisateur.
Déroulement :
- Avant d'exécuter l'outil, le plugin agents émet un événement SSE
appkit.approval_pendingcontenant les champsapproval_id,stream_id,tool_name,argsetannotationsde l'appel en attente. - Le client de chat affiche une demande d'approbation (voir la carte d'approbation de l'application de référence).
L'utilisateur qui a lancé le flux envoie sa décision à
POST /api/agents/approve:POST /api/agents/approve Content-Type: application/json X-Forwarded-User: <end-user id> X-Forwarded-Access-Token: <OBO token> { "streamId": "...", "approvalId": "...", "decision": "approve" | "deny" }- En cas d'approbation, l'outil s'exécute normalement et le flux se poursuit. En cas de refus, l'adaptateur reçoit la chaîne
"Tool execution denied by user approval gate (tool: <name>)."comme output de l'outil, et le LLM peut s'excuser ou revoir son plan. Si aucune décision n'arrive dans le délaiapproval.timeoutMs(60 s par défaut), le contrôle refuse automatiquement.
La route impose que le décideur soit le propriétaire du flux : une approbation provenant d'un x-forwarded-user différent renvoie 403. Annuler le flux via POST /api/agents/cancel refuse toutes les approbations en attente sur ce flux.
Limites de ressources
Le plugin applique quelques plafonds afin de protéger un déploiement à instance unique contre les prompts incontrôlés, les clients au comportement anormal ou les cycles de délégation provoqués par injection de prompt. Certains sont statiques (imposés par le schéma de requête), d'autres sont configurables via agents({ limits: { ... } }).
Plafonds statiques (appliqués lors de l'analyse des requêtes POST /chat, POST /invocations et POST /responses) :
| Champ | Plafond | Pourquoi |
|---|---|---|
chat.message | 64 000 caractères | ~16k tokens ; au-delà, il s'agit presque à coup sûr d'un abus. |
chaîne invocations.input | 64 000 caractères | Même raisonnement. |
tableau invocations.input | 100 éléments | Empêche qu'une seule requête injecte des centaines de messages dans le magasin de threads. |
chaîne invocations.input[].content | 64 000 caractères | Plafond par message injecté. |
tableau invocations.input[].content | 100 éléments | Plafond par message injecté. |
Plafonds configurables (valeurs par défaut indiquées) :
agents({
limits: {
maxConcurrentStreamsPerUser: 5, // HTTP 429 + Retry-After en cas de dépassement
maxToolCalls: 50, // interrompt le run si le budget est épuisé
maxSubAgentDepth: 3, // refuse la récursion de sous-agents au-delà de cette valeur
toolCallTimeoutMs: 300_000, // délai d'expiration par appel d'outil (5 min ; marge pour SQL/Genie à froid)
},
});Le budget maxToolCalls est partagé entre l'adaptateur de premier niveau et tous les sous-agents auxquels il délègue : une propagation en éventail injectée via un prompt ne peut donc pas y échapper en descendant d'un niveau. maxConcurrentStreamsPerUser s'applique par utilisateur, et non globalement — un utilisateur qui atteint sa limite n'affecte pas les autres.
API runtime
Après createApp, le plugin expose :
appkit.agents.list(); // => ["support", "researcher", ...]
appkit.agents.get("support"); // => RegisteredAgent | null
appkit.agents.getDefault(); // => "support"
appkit.agents.register(name, def); // enregistrement dynamique
appkit.agents.reload(); // ré-analyse le répertoire
appkit.agents.getThreads(userId); // liste les fils de l'utilisateurÉvaluer les agents
AppKit embarque un framework d'évaluation pour les agents que vous développez ici. Vous rédigez les évaluations en TypeScript avec defineEval, pilotez l'agent en lui envoyant des messages, puis validez sa réponse et son utilisation des outils à l'aide de matchers déterministes ou de juges LLM. Les évaluations s'exécutent sur une application en cours d'exécution via HTTP (--url) et — avec des identifiants Databricks et une expérience — sont remontées à MLflow sous forme d'« Evaluation runs » natives, le feedback de chaque assertion et de chaque juge étant rattaché à la trace de chaque tour. L'API d'évaluation fait partie de la surface bêta : importez-la depuis @databricks/appkit/beta.
Les évaluations se trouvent aux côtés de chaque agent : server/agents/<agent-id>/evals/*.eval.ts. Chaque fichier exporte par défaut un unique defineEval({ test }). L'agent testé correspond par défaut au répertoire parent <agent-id> ; définissez agent: pour en cibler un autre.
Une première évaluation
// server/agents/query/evals/smoke.eval.ts
import { defineEval } from "@databricks/appkit/beta";
export default defineEval({
description: "Query agent responds to a greeting",
async test(t) {
await t.send("Hi there!");
t.succeeded(); // condition : le tour s'est terminé sans erreur d'agent ni de flux
},
});Démarrez l'application, puis lancez les évaluations dessus :
# l'application doit être démarrée et accessible à l'adresse indiquée par --url
appkit agent eval --url http://localhost:3000
# restreindre à un seul agent/eval via une sous-chaîne, et cibler une racine de projet
appkit agent eval query --root apps/dev-playground --url http://localhost:3000Le paramètre positionnel [filter] sélectionne les évaluations dont le <agent>/<id> contient la sous-chaîne indiquée (ou correspond exactement à un id d'agent). La commande détecte tous les fichiers *.eval.ts présents sous server/agents/*/evals/, les exécute chacun sur l'application en cours d'exécution et retourne un code de sortie non nul si un critère n'est pas respecté.
Assertions
Chaque assertion renvoie un handle chaînable. Les assertions sont bloquantes par défaut : un échec fait échouer l'évaluation (code de sortie non nul). Chaînez .soft() pour la rétrograder en simple métrique suivie, .gate() pour repromouvoir une assertion souple en assertion bloquante, ou .atLeast(n) pour définir le seuil de réussite d'une assertion notée.
| Assertion | Réussit lorsque |
|---|---|
t.succeeded() | Le dernier tour s'est terminé sans erreur d'agent ou de flux. |
t.calledTool(name) | L'agent a appelé name pendant le run. |
t.calledToolWith(name, expected) | name a été appelé avec des arguments qui contiennent en profondeur expected (chaque clé de expected correspond récursivement ; les arguments supplémentaires sont ignorés). |
t.check(value, matcher) | value satisfait le matcher — includes(substring), equals(expected) ou matches(pattern). |
import { defineEval, includes } from "@databricks/appkit/beta";
export default defineEval({
description: "Helper agent answers a math question",
agent: "helper",
async test(t) {
await t.send("What is 2 + 2?");
t.succeeded(); // gate
t.check(t.reply, includes("4")).soft(); // métrique suivie, ne fait pas échouer le gate
},
});// vérification partielle en profondeur des arguments de l'outil
await t.send("What's the weather in Brooklyn?");
t.calledTool("get_weather");
t.calledToolWith("get_weather", { city: "Brooklyn" });Appelez t.skip("reason") pour ignorer une évaluation, et lisez t.reply, t.toolCalls et t.sessionId pour inspecter le dernier tour de conversation.
LLM-as-judge
t.judge.* note la dernière réponse à l'aide d'un juge LLM (via autoevals pointant vers un serving endpoint Databricks). Chaque juge renvoie une assertion notée (0..1) qui bloque par défaut : un score insuffisant fait échouer l'évaluation. Chaînez .atLeast(n) pour définir le seuil de réussite, ou .soft() pour vous contenter d'en assurer le suivi. Les juges requièrent un modèle de juge : passez --judge-model <endpoint> (ou définissez APPKIT_JUDGE_MODEL), en plus de l'authentification Databricks ; sans cela, t.judge.* lève une erreur accompagnée d'un message explicite.
async test(t) {
await t.send("What's the weather in Brooklyn?");
t.succeeded();
// closedQA ne nécessite aucune vérité terrain — il évalue la réponse au regard d'une question.
(await t.judge.closedQA(
"Does the response describe weather conditions for Brooklyn?",
)).atLeast(0.5);
}t.judge.factuality(expected)— note la réponse par rapport à une réponse de référence attendue.t.judge.closedQA(criteria)— détermine si la réponse répond bien à la question, seloncriteria.t.judge.custom(spec)— un juge basé sur un modèle de prompt ({ name, promptTemplate, choiceScores }), l'équivalent TS du@scorerde MLflow.
Encadrez les appels au juge avec isJudgeConfigured() lorsqu'une évaluation doit tout de même parcourir le chemin d'exécution sans qu'un modèle de juge soit configuré :
import { defineEval, isJudgeConfigured } from "@databricks/appkit/beta";
// ...
if (isJudgeConfigured()) {
(await t.judge.closedQA(guideline)).atLeast(0.5);
}Conversations
Chaque t.send correspond à un tour de parole de l'utilisateur. L'ordre dans lequel vous les enchaînez détermine le déroulement du fil :
// One-shot : un seul tour de parole.
await t.send("Summarize Q3 revenue.");
t.succeeded();
// Multi-tours : les envois consécutifs partagent un même fil, l'agent voit donc l'historique.
await t.send("Show me the orders table.");
await t.send("Now filter it to last week.");
t.succeeded();
// t.reset() abandonne la conversation : l'envoi suivant ouvre un nouveau fil,
// sans historique. Pratique pour enchaîner plusieurs vérifications one-shot indépendantes dans un même test.
await t.send("What's 2 + 2?");
t.check(t.reply, includes("4"));
t.reset();
await t.send("What's the capital of France?");
t.check(t.reply, includes("Paris"));Datasets
Ajoutez dataset: { table } pour balayer un dataset d'évaluation géré par Databricks — une table Unity Catalog catalog.schema.table comportant les colonnes inputs/expectations. L'évaluation s'exécute une fois par ligne ; le runner associe les inputs de chaque ligne à t.input et les expectations à t.expected. La lecture du dataset nécessite un client workspace et un warehouse (--warehouse-id + authentification).
import { defineEval, isJudgeConfigured, userTurns } from "@databricks/appkit/beta";
export default defineEval({
description: "Query agent satisfies each dataset row's guidelines",
dataset: { table: "main.mario.appkit_eval_dataset" }, // `limit?` facultatif
async test(t) {
// Rejoue chaque tour utilisateur de la ligne dans un seul thread, afin que l'agent
// voie la conversation s'enrichir. Une ligne à un seul tour utilisateur n'envoie qu'un message.
for (const turn of userTurns(t.input)) {
await t.send(turn);
}
t.succeeded();
if (isJudgeConfigured()) {
for (const guideline of guidelines(t.expected)) {
(await t.judge.closedQA(guideline)).atLeast(0.5);
}
}
},
});La structure des lignes correspond à l'interface des datasets gérés MLflow :
inputs {"messages":[{"role":"user","content":"..."}]}
expectations {"guidelines":{"value":["...","..."]}} (optional)userTurns(t.input) extrait dans l'ordre le contenu de chaque message role: "user" de l'entrée {messages:[...]} — une ligne peut contenir une conversation complète à plusieurs tours, et rejouer chaque tour utilisateur dans un même fil permet à l'agent de constituer son historique (les tours assistant/système intercalés sont ignorés ; l'agent génère les siens). Lisez vous-même expectations.guidelines.value ; l'interface encapsule le tableau sous la forme {value: [...]} :
function guidelines(expected: Record<string, unknown> | undefined): string[] {
const g = (expected?.guidelines as { value?: unknown } | undefined)?.value;
return Array.isArray(g) ? g.map(String) : [];
}Exécutez une évaluation sur un dataset :
appkit agent eval dataset --root apps/dev-playground --url http://localhost:3000 \
--profile <profile> --warehouse-id <warehouse-id> --judge-model <endpoint>Exécuter les évaluations et la CI
appkit agent eval [filter] — exécute les évaluations d'agents (server/agents/<id>/evals/*.eval.ts) sur une application en cours d'exécution.
| Option | Description |
|---|---|
[filter] | N'exécute que les évaluations dont <agent>/<id> contient cette sous-chaîne (ou un id d'agent exact) |
--url <url> | URL de base de l'application en cours d'exécution (par défaut http://localhost:3000) |
--strict | Échoue également en cas d'assertions souples non satisfaites |
--root <dir> | Racine du projet contenant server/agents/ (par défaut : cwd) |
--header <header...> | En-tête de requête supplémentaire sous la forme 'Key: value' (répétable) |
--tag <tag...> | N'exécute que les évaluations portant l'une de ces étiquettes (répétable) |
--profile <name> | Profil du CLI Databricks à utiliser pour s'authentifier via OAuth (par défaut : DATABRICKS_CONFIG_PROFILE) |
--databricks-host <host> | Hôte Databricks pour l'écriture des évaluations MLflow (par défaut : DATABRICKS_HOST) |
--databricks-token <token> | Jeton Databricks pour l'écriture des évaluations MLflow (par défaut : DATABRICKS_TOKEN) |
--experiment <id> | Id d'expérience MLflow pour l'exécution d'évaluation (par défaut : MLFLOW_EXPERIMENT_ID) |
--warehouse-id <id> | Id du SQL warehouse pour la lecture des datasets d'évaluation gérés (par défaut : DATABRICKS_WAREHOUSE_ID) |
--judge-model <endpoint> | Serving endpoint Databricks à utiliser comme juge LLM pour t.judge.* (par défaut : APPKIT_JUDGE_MODEL) |
--concurrency <n> | Nombre maximal d'évaluations/lignes de dataset traitées simultanément (par défaut : 4) |
--timeout <ms> | Délai d'expiration par défaut par évaluation, en ms (un timeoutMs défini au niveau d'une évaluation le remplace) |
--retries <n> | Relance une évaluation jusqu'à N fois lorsqu'elle échoue sur une erreur d'infrastructure (tour/expiration) ; les échecs d'assertion ne sont pas relancés |
--min-pass-rate <rate> | Conditionne le résultat au taux de réussite global (0..1) plutôt que d'exiger la réussite de chaque évaluation ; sort avec le code 1 en dessous |
--reporter <format> | Format du rapport : text (console en direct), json (dashboards) ou junit (rapporteurs de tests CI) |
--output <file> | Écrit le rapport json/junit dans ce fichier au lieu de stdout (ignoré pour text) |
Remarques :
- L'authentification privilégie OAuth.
--profile <name>génère un OAuth token à partir de votre profil du CLI Databricks — aucun PAT n'est nécessaire. Un--databricks-host/--databricks-tokenexplicite (ou les variables d'environnementDATABRICKS_*) prime sur le profil. --retriesn'absorbe que l'instabilité de l'infrastructure. Une relance ne se déclenche que lorsqu'une évaluation lève une erreur ou expire (result.errordéfini) ; une réponse erronée constitue un vrai signal et n'est jamais relancée. Chaque tentative repart d'un driver neuf.- Conditionnement. Par défaut, l'exécution se termine avec un code non nul dès qu'une évaluation échoue.
--min-pass-rate 0.9bascule en mode seuil : le code de sortie n'est non nul que si le taux de réussite global passe sous le seuil.--stricttraite en outre les assertions souples non satisfaites comme des échecs. - Rapports CI.
--reporter junit --output results.xmlécrit un fichier JUnit pour les rapporteurs de tests CI ;--reporter jsonproduit des résultats exploitables par des outils. Avec les rapporteurs destinés aux machines, les lignes destinées aux humains sont envoyées vers stderr afin que stdout reste réservé au rapport.
# CI : seuil de réussite fixé à 90 %, génération d'un rapport JUnit, authentification + envoi des résultats à MLflow
appkit agent eval --url "$APP_URL" \
--profile ci \
--experiment "$MLFLOW_EXPERIMENT_ID" \
--concurrency 4 --retries 1 \
--min-pass-rate 0.9 \
--reporter junit --output eval-results.xmlConfiguration par répertoire
Placez un fichier evals.config.ts à côté des évaluations d'un agent pour définir les valeurs par défaut des runs de cet agent :
// server/agents/query/evals/evals.config.ts
import { defineEvalConfig } from "@databricks/appkit/beta";
export default defineEvalConfig({
maxConcurrency: 4, // exécute jusqu'à 4 évaluations/lignes en parallèle
timeoutMs: 30_000, // délai d'expiration par défaut par évaluation
});Ordre de priorité : une option CLI l'emporte sur la valeur définie dans evals.config.ts, qui l'emporte elle-même sur la valeur par défaut intégrée (concurrence 4, aucun délai d'expiration). Un def.timeoutMs propre à une évaluation prévaut sur les deux pour cette évaluation.
Rapports MLflow
Lorsque --experiment <id> (ou MLFLOW_EXPERIMENT_ID) est défini en combinaison avec l'authentification Databricks, le runner crée d'emblée un Evaluation run MLflow natif. À mesure que chaque évaluation s'exécute sur l'application, sa trace de tour est rattachée au run, et chaque assertion ainsi que chaque score de juge y sont réinscrits sous forme de feedback :
- Chaque assertion déterministe devient un Feedback de source
CODEassorti d'une valeur booléenne. - Chaque assertion de juge devient un Feedback de source
LLM_JUDGEassorti de son score numérique de 0 à 1 et de sa justification. - Un Feedback global
appkit_evalde réussite/échec est attaché à chaque évaluation, et des métriques agrégées sont journalisées à la fin du run.
Omettez --experiment (et MLFLOW_EXPERIMENT_ID) pour exécuter les évaluations uniquement en local, sans effet de bord MLflow — la CLI affiche alors un rappel indiquant que le run d'évaluation a été ignoré.
Schéma du frontmatter
| Clé | Type | Remarques | |
|---|---|---|---|
endpoint | string | Nom du serving endpoint du modèle. Raccourci pour model. | |
model | string | Identique à endpoint ; les deux fonctionnent. | |
tools | array | Liste unifiée d'outils. Les entrées prennent la forme plugin:<name> / plugin:<name>: [t1, t2] / plugin:<name>: { only, except, rename, prefix } pour les outils de plugin, ou d'une simple <key> résolue via agents({ tools: {...} }) pour les outils ambiants. Voir « Niveau 2 : restreindre les outils dans le frontmatter » ci-dessus pour des exemples. | |
skills | array | Noms des compétences globales (pool skills/ partagé ou volume de catalogue) à rendre visibles pour cet agent. Les compétences propres à un agent, sous <id>/skills/, sont toujours visibles. Voir Compétences. | |
default | boolean | Le premier id d'agent (par ordre trié) portant default: true devient l'agent par défaut. | |
agents | array | Ids des sous-agents (dossiers frères) auxquels déléguer ; chacun devient un outil agent-<id>. La résolution se fait parmi les autres agents markdown et code. | |
maxSteps | number | Indication du nombre maximal d'étapes pour l'adaptateur. | |
maxTokens | number | Indication du nombre maximal de tokens pour l'adaptateur. | |
generationParams | object | Paramètres de génération de l'adaptateur (p. ex. temperature, top_p) transmis lorsque AppKit construit l'adaptateur. | |
baseSystemPrompt | false | string | Surcharge par agent. false désactive le prompt de base d'AppKit. |
ephemeral | boolean | Si true, le fil créé pour une requête de chat adressée à cet agent est supprimé du ThreadStore une fois le flux terminé. À utiliser pour des agents sans état à usage unique (p. ex. autocomplétion), afin que l'historique ne s'accumule pas et ne vienne pas fausser les appels suivants. Vaut false par défaut. |
Les clés inconnues sont journalisées puis ignorées. Un YAML invalide ou des références manquantes à un plugin ou à un outil provoquent une erreur au démarrage.