Pendant deux ans, construire un agent de code signifiait écrire la boucle soi-même : appeler le modèle, analyser les appels d'outils, les exécuter, réinjecter les résultats, recommencer jusqu'à la fin. Tout le monde a reconstruit la même mécanique, et tout le monde s'est trompé subtilement sur les parties difficiles — compaction du contexte, récupération après erreur d'outil, frontières de permissions, reprise après un crash.
AI SDK 7 adopte une position différente. Plutôt que de vous donner de meilleures primitives pour construire une boucle, il vous donne les boucles qui fonctionnent déjà. HarnessAgent encapsule les harnais d'agents éprouvés — Claude Code, Codex, Pi — derrière une seule API TypeScript, puis vous laisse configurer ce à quoi ils peuvent toucher : dans quelle sandbox ils s'exécutent, quels skills ils chargent, quels outils exigent une approbation humaine.
Ce tutoriel construit un véritable agent de revue de pull requests GitHub avec cette pile. À la fin, vous disposerez d'un agent qui clone un dépôt dans une sandbox isolée, relit une PR avec un skill que vous avez écrit, demande la permission avant de publier quoi que ce soit de public, et survit à un redémarrage du serveur en cours d'exécution.
Pourquoi un harnais plutôt que votre propre boucle ? Un harnais, c'est le réglage accumulé d'un agent de code en production — structure des instructions, gestion du contexte, schémas d'outils, comportement de retry. HarnessAgent vous permet d'hériter de ce travail et de consacrer votre effort à ce qui vous appartient réellement : la logique métier, les garde-fous et la surface d'intégration.
Prérequis
Avant de commencer, assurez-vous de disposer de :
- Node.js 20 ou plus récent (Node 24 recommandé — le runtime sandbox le cible)
- TypeScript 5.5+ et une familiarité avec l'itération asynchrone
- Un compte Vercel si vous voulez des sandboxes hébergées (les sandboxes locales fonctionnent sans)
- Une clé API pour au moins un fournisseur — Anthropic pour Claude Code, OpenAI pour Codex
- Un token d'accès personnel GitHub avec la portée
repopour l'exemple de revue - Une connaissance pratique du helper
tool()de l'AI SDK — si vous débutez, commencez par notre tutoriel agents et streaming avec AI SDK 5
Ce que vous allez construire
Un agent de revue de PR en ligne de commande qui :
- Accepte un dépôt GitHub et un numéro de pull request
- Démarre une sandbox isolée avec le dépôt cloné
- Exécute Claude Code dans cette sandbox avec un skill de revue personnalisé
- Lit le diff de la PR et les issues liées via des outils typés
- Exige une approbation explicite avant de publier un commentaire de revue
- Persiste son état pour qu'une exécution interrompue reprenne au lieu de redémarrer
Nous le construirons par couches, en l'exécutant à chaque étape.
Étape 1 : mise en place du projet
Créez le projet et installez les paquets AI SDK 7 :
mkdir pr-review-agent && cd pr-review-agent
pnpm init
pnpm add ai@latest zod
pnpm add @ai-sdk/anthropic @ai-sdk/openai
pnpm add @ai-sdk/sandbox @ai-sdk/workflow @ai-sdk/tui
pnpm add -D typescript tsx @types/nodeUn tsconfig.json minimal :
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}Définissez "type": "module" dans package.json, puis créez votre fichier d'environnement :
# .env
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GITHUB_TOKEN=ghp_...
VERCEL_OIDC_TOKEN=... # nécessaire uniquement pour les sandboxes hébergéesNe committez jamais ce fichier. Ajoutez .env à .gitignore avant votre premier commit. Les agents qui exécutent des commandes shell rendent une fuite d'identifiants nettement plus dangereuse que d'habitude — une sandbox détenant votre token de production n'est isolée que de nom.
Étape 2 : votre premier HarnessAgent
Commencez par la plus petite chose qui s'exécute. Créez src/hello-agent.ts :
import 'dotenv/config';
import { HarnessAgent } from 'ai';
import { claudeCode } from 'ai/harnesses';
const agent = new HarnessAgent({
harness: claudeCode,
instructions:
'You are a careful code reviewer. Be concise and specific. ' +
'Prefer concrete line references over general advice.',
});
const result = await agent.generate({
prompt: 'Explain what a race condition is in a Node.js HTTP handler, in three sentences.',
});
console.log(result.text);Exécutez-le :
pnpm tsx src/hello-agent.tsTrois points méritent attention. Vous n'avez écrit aucune boucle — HarnessAgent la possède. Vous n'avez pas décrit le format d'appel d'outils au modèle — le harnais apporte le sien. Et instructions se superpose au prompt système du harnais au lieu de le remplacer : vous orientez le comportement sans jeter le réglage pour lequel vous êtes venu.
Changer de harnais tient en une ligne :
import { codex } from 'ai/harnesses';
const agent = new HarnessAgent({
harness: codex, // [!code highlight]
instructions: 'You are a careful code reviewer...',
});Étape 3 : ajouter une sandbox
Pour l'instant, l'agent n'a aucun environnement d'exécution. Donnez-lui-en un. Une sandbox est un système de fichiers et un espace de processus isolés où l'agent peut lancer des commandes shell sans toucher à votre machine.
Créez src/sandboxed-agent.ts :
import 'dotenv/config';
import { HarnessAgent } from 'ai';
import { claudeCode } from 'ai/harnesses';
import { createVercelSandbox } from '@ai-sdk/sandbox';
const agent = new HarnessAgent({
harness: claudeCode,
sandbox: createVercelSandbox({
runtime: 'node24',
ports: [4000],
timeoutMs: 10 * 60 * 1000,
}),
instructions:
'You have a sandbox with Node 24. Use shell commands to inspect and test code. ' +
'Always run the test suite before concluding that a change is safe.',
});
const result = await agent.generate({
prompt:
'Create a small Node script that computes the 40th Fibonacci number ' +
'both recursively and iteratively, then benchmark both and report the difference.',
});
console.log(result.text);L'agent écrit désormais des fichiers, lance node, lit la sortie et raisonne dessus — le tout dans un conteneur qui disparaît à la fin de l'exécution.
En développement local, vous pouvez éviter l'aller-retour vers l'hébergeur :
import { createLocalSandbox } from '@ai-sdk/sandbox';
const sandbox = createLocalSandbox({
cwd: './workspace',
allowedCommands: ['node', 'npm', 'pnpm', 'git', 'ls', 'cat', 'grep'],
});allowedCommands est une vraie frontière, pas une suggestion. Une sandbox locale partage votre système de fichiers. Sans liste blanche, « lance les tests » et « supprime le dépôt » relèvent de la même catégorie d'action pour l'agent. En production, préférez une sandbox hébergée — l'isolation y est imposée par le conteneur, pas par une comparaison de chaînes.
Étape 4 : packager la connaissance métier en skill
Les instructions deviennent vite ingérables. Dès que votre prompt contient des checklists et des conventions d'équipe, déplacez-le dans un skill — un bloc d'instructions nommé, décrit et réutilisable que le harnais charge à la demande.
Créez src/skills/review-pr.ts :
export const reviewPullRequestSkill = {
name: 'review-github-pr',
description:
'Reviews a GitHub pull request against the team engineering standards. ' +
'Use whenever the user asks for a PR review, code review, or diff assessment.',
content: `
# Pull Request Review
## Procedure
1. Read the PR title, body, and linked issues to establish intent.
2. Read the full diff before commenting on any single file.
3. Check the diff against the review checklist below.
4. Verify claims by running code in the sandbox — never assert that a test
passes without running it.
5. Produce the output in the required format.
## Review checklist
- **Correctness:** off-by-one errors, unhandled null and undefined,
incorrect async ordering, missing await.
- **Error handling:** every catch block either handles or rethrows.
Empty catch blocks are always a finding.
- **Security:** unvalidated input reaching a query, a shell command,
or a filesystem path. Secrets in source.
- **Tests:** does each behavioural change have a corresponding test?
- **Scope:** does the diff do anything the PR description does not mention?
## Output format
For each finding emit exactly:
**[severity] file:line** — one-sentence description of the defect.
Then a concrete failure scenario: specific inputs leading to specific wrong output.
Severity is one of: blocker, major, minor, nit.
If you find nothing, say so plainly. Do not invent findings to appear thorough.
`,
};Attachez-le :
const agent = new HarnessAgent({
harness: claudeCode,
sandbox: createVercelSandbox({ runtime: 'node24' }),
skills: [reviewPullRequestSkill], // [!code highlight]
instructions: 'You are a senior engineer reviewing code for a TypeScript team.',
});Le champ description est la pièce porteuse. Le harnais lit les descriptions pour décider quand un skill est pertinent : écrivez-la donc comme une condition de déclenchement (« à utiliser dès que l'utilisateur demande… »), pas comme un résumé.
Pour les harnais qui s'exécutent dans des conteneurs gérés par le fournisseur, téléversez le skill une fois et référencez-le par identifiant plutôt que de renvoyer son contenu à chaque appel :
import { uploadSkill } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { readFileSync } from 'node:fs';
const { providerReference } = await uploadSkill({
api: anthropic.skills(),
files: [
{ path: 'review-pr/SKILL.md', content: readFileSync('./skills/review-pr/SKILL.md') },
],
displayTitle: 'PR Review Standards',
});Étape 5 : donner des outils typés à l'agent
Les skills disent à l'agent comment penser. Les outils lui donnent une portée. Notre agent a besoin de GitHub : définissons des outils avec des schémas Zod.
Créez src/tools/github.ts :
import { tool } from 'ai';
import { z } from 'zod';
const GITHUB_API = 'https://api.github.com';
async function gh(path: string, token: string, init?: RequestInit) {
const response = await fetch(`${GITHUB_API}${path}`, {
...init,
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
...init?.headers,
},
});
if (!response.ok) {
throw new Error(`GitHub ${response.status}: ${await response.text()}`);
}
return response;
}
export const readPullRequest = tool({
description: 'Fetch a pull request title, body, author, and unified diff.',
inputSchema: z.object({
owner: z.string().describe('Repository owner, e.g. "vercel"'),
repo: z.string().describe('Repository name, e.g. "ai"'),
number: z.number().int().positive().describe('Pull request number'),
}),
contextSchema: z.object({ token: z.string() }),
execute: async ({ owner, repo, number }, { context: { token } }) => {
const meta = await gh(`/repos/${owner}/${repo}/pulls/${number}`, token).then((r) => r.json());
const diff = await gh(`/repos/${owner}/${repo}/pulls/${number}`, token, {
headers: { Accept: 'application/vnd.github.v3.diff' },
}).then((r) => r.text());
return {
title: meta.title,
body: meta.body ?? '',
author: meta.user?.login,
changedFiles: meta.changed_files,
additions: meta.additions,
deletions: meta.deletions,
diff: diff.slice(0, 120_000),
};
},
});
export const postReviewComment = tool({
description:
'Post a review comment on a pull request. This is publicly visible and cannot be undone.',
inputSchema: z.object({
owner: z.string(),
repo: z.string(),
number: z.number().int().positive(),
body: z.string().min(1).describe('Markdown body of the review comment'),
}),
contextSchema: z.object({ token: z.string() }),
execute: async ({ owner, repo, number, body }, { context: { token } }) => {
const result = await gh(`/repos/${owner}/${repo}/issues/${number}/comments`, token, {
method: 'POST',
body: JSON.stringify({ body }),
}).then((r) => r.json());
return { url: result.html_url, id: result.id };
},
});Deux détails comptent ici.
contextSchema garde le token GitHub entièrement hors des entrées du modèle. L'agent choisit quelle PR lire ; c'est le runtime qui fournit l'identifiant. Le modèle ne voit jamais le token et ne peut donc pas le faire fuiter dans une réponse, une ligne de log ou un fichier écrit dans la sandbox.
Le plafond diff.slice(0, 120_000) est délibéré. Une grosse PR peut produire un diff qui consomme toute la fenêtre de contexte sans laisser de place au raisonnement. Tronquer à une limite connue échoue de façon prévisible plutôt que mystérieuse.
Étape 6 : protéger les outils sensibles par approbation
readPullRequest est inoffensif. postReviewComment publie quelque chose de public en votre nom. AI SDK 7 vous permet d'exprimer cette distinction de manière déclarative :
import { HarnessAgent } from 'ai';
import { claudeCode } from 'ai/harnesses';
import { createVercelSandbox } from '@ai-sdk/sandbox';
import { readPullRequest, postReviewComment } from './tools/github.js';
import { reviewPullRequestSkill } from './skills/review-pr.js';
export const reviewAgent = new HarnessAgent({
harness: claudeCode,
sandbox: createVercelSandbox({ runtime: 'node24', timeoutMs: 15 * 60 * 1000 }),
skills: [reviewPullRequestSkill],
tools: { readPullRequest, postReviewComment },
toolApproval: {
postReviewComment: 'user-approval', // [!code highlight]
},
toolContext: { token: process.env.GITHUB_TOKEN! },
instructions:
'You are a senior engineer reviewing code for a TypeScript team. ' +
'Review thoroughly before posting anything. Post at most one comment per run.',
});Quand l'agent appelle postReviewComment, l'exécution se suspend et fait remonter une demande d'approbation au lieu d'exécuter. Traitez-la en itérant sur le flux :
const stream = await reviewAgent.stream({
prompt: 'Review pull request 412 in vercel/ai and post your findings.',
});
for await (const part of stream.fullStream) {
if (part.type === 'tool-approval-request') {
console.log(`\nAgent wants to call: ${part.toolName}`);
console.log(JSON.stringify(part.input, null, 2));
const approved = await askUser('Approve? (y/n) ');
await stream.respondToApproval({ id: part.id, approved });
}
if (part.type === 'text-delta') process.stdout.write(part.text);
}Vous pouvez aussi exprimer la politique sous forme de fonction lorsque la décision dépend des arguments plutôt que de l'identité de l'outil :
toolApproval: {
postReviewComment: async ({ input }) =>
input.body.length > 2000 ? 'user-approval' : 'auto-approve',
}L'approbation est une frontière, pas un raffinement d'UX. La règle qui tient en production : tout outil dont l'effet est visible hors de votre processus — publier, envoyer un e-mail, déployer, payer, supprimer — est soumis à approbation par défaut. Les outils en lecture seule s'exécutent librement. Cette seule classification prévient la plupart des modes de défaillance qui font perdre confiance aux équipes.
Étape 7 : ajouter timeouts et observabilité
Un agent qui se fige est pire qu'un agent qui échoue. AI SDK 7 expose des timeouts en couches :
const result = await reviewAgent.generate({
prompt: 'Review pull request 412 in vercel/ai.',
timeout: {
totalMs: 15 * 60 * 1000, // exécution complète
stepMs: 90_000, // toute étape isolée
chunkMs: 20_000, // écart entre fragments du flux
toolMs: 30_000, // valeur par défaut par appel d'outil
tools: {
readPullRequestMs: 45_000, // les gros diffs demandent plus de temps
},
},
});chunkMs est le paramètre que l'on saute puis que l'on regrette — il attrape une connexion fournisseur bloquée qui, sinon, resterait ouverte jusqu'à expiration de totalMs.
Pour le traçage, enregistrez la télémétrie une seule fois au démarrage :
import { registerTelemetry } from 'ai';
import { OpenTelemetry } from '@ai-sdk/opentelemetry';
registerTelemetry(new OpenTelemetry());Puis lisez les métriques d'exécution sur l'étape finale :
const { performance } = await result.finalStep;
console.log({
responseTimeMs: performance.responseTimeMs,
outputTokensPerSecond: performance.outputTokensPerSecond,
timeToFirstOutputMs: performance.timeToFirstOutputMs,
totalTokens: result.usage.totalTokens,
});Si vous disposez déjà d'une stack d'observabilité, notre tutoriel Langfuse pour l'observabilité LLM montre comment router ces traces vers un tableau de bord avec attribution des coûts au niveau du prompt.
Étape 8 : rendre les exécutions durables
Une revue de PR peut durer dix minutes. Déploiements, redémarrages et scale-to-zero surviennent tous dans cette fenêtre. WorkflowAgent persiste l'état de l'agent entre les étapes pour qu'une exécution interrompue reprenne à son dernier point de contrôle.
import { WorkflowAgent } from '@ai-sdk/workflow';
import { claudeCode } from 'ai/harnesses';
import { createVercelSandbox } from '@ai-sdk/sandbox';
import { readPullRequest, postReviewComment } from './tools/github.js';
import { reviewPullRequestSkill } from './skills/review-pr.js';
export const durableReviewAgent = new WorkflowAgent({
harness: claudeCode,
sandbox: createVercelSandbox({ runtime: 'node24' }),
skills: [reviewPullRequestSkill],
tools: { readPullRequest, postReviewComment },
toolApproval: { postReviewComment: 'user-approval' },
toolContext: { token: process.env.GITHUB_TOKEN! },
workflowId: (input) => `pr-review-${input.owner}-${input.repo}-${input.number}`,
});Un workflowId déterministe vous offre l'idempotence gratuitement : relancer la même PR reprend l'exécution existante au lieu d'ouvrir une revue en double.
Branchez-le sur un webhook pour déclencher les revues sur les événements de PR :
// app/api/github-webhook/route.ts
import { durableReviewAgent } from '@/lib/review-agent';
export async function POST(req: Request) {
const event = await req.json();
if (event.action !== 'opened' && event.action !== 'synchronize') {
return new Response('ignored', { status: 200 });
}
await durableReviewAgent.trigger({
owner: event.repository.owner.login,
repo: event.repository.name,
number: event.pull_request.number,
prompt: `Review pull request ${event.pull_request.number}.`,
});
return new Response('queued', { status: 202 });
}Comme trigger retourne dès que l'exécution est mise en file de façon durable, le webhook répond largement dans le délai imparti par GitHub pendant que l'agent poursuit son travail.
Étape 9 : le piloter depuis un terminal
Pour l'itération locale, @ai-sdk/tui vous donne une session interactive en quelques lignes :
// src/cli.ts
import 'dotenv/config';
import { runAgentTUI } from '@ai-sdk/tui';
import { reviewAgent } from './review-agent.js';
await runAgentTUI({
agent: reviewAgent,
title: 'PR Review Agent',
onApprovalRequest: async ({ toolName, input }) => ({
approved: await confirmInTerminal(`Call ${toolName}?`, input),
}),
});pnpm tsx src/cli.tsL'interface terminal affiche le raisonnement, les appels d'outils et le markdown sous forme de texte formaté, et gère les demandes d'approbation en ligne. C'est le moyen le plus rapide d'observer ce que votre agent fait réellement avant de le placer derrière un webhook.
Tester votre implémentation
Vérifiez chaque couche indépendamment :
Le harnais répond. Lancez src/hello-agent.ts. Vous devriez obtenir trois phrases sans aucun appel d'outil.
La sandbox exécute. Lancez src/sandboxed-agent.ts et vérifiez que la sortie contient de vrais chiffres de benchmark. Si l'agent rapporte des temps sans rien avoir exécuté, la sandbox n'est pas attachée — vérifiez que createVercelSandbox est bien passé et que votre token est valide.
Le skill se charge. Demandez une revue de PR et vérifiez que la sortie suit votre format **[severity] file:line**. Sinon, la description du skill ne correspond pas — rendez-la plus explicitement déclencheuse.
L'approbation bloque. Demandez à l'agent de publier un commentaire et répondez n. Vérifiez via l'API GitHub qu'aucun commentaire n'existe :
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/OWNER/REPO/issues/412/comments" | jq 'length'La durabilité tient. Déclenchez une exécution durable, tuez le processus après le premier appel d'outil, redémarrez et relancez avec le même workflowId. L'exécution doit reprendre au lieu de relire le diff depuis zéro.
Dépannage
Sandbox timed out sur de gros dépôts. Le budget par défaut est souvent trop court pour une installation complète. Augmentez timeoutMs sur la sandbox et donnez à readPullRequest son propre toolMs plus long. Si l'installation domine, préchauffez l'image de sandbox avec les dépendances intégrées.
L'agent ignore le skill. C'est presque toujours un problème de description. « Relit les pull requests » est un résumé ; « à utiliser dès que l'utilisateur demande une revue de PR, une revue de code ou une évaluation de diff » est un déclencheur. Écrivez des déclencheurs.
Les demandes d'approbation n'arrivent jamais. generate() ne peut pas les faire remonter — elle ne se résout qu'à la fin de l'exécution. Les approbations exigent stream() et l'itération sur fullStream, ou le callback onApprovalRequest de l'interface terminal.
Fenêtre de contexte saturée sur les gros diffs. Baissez le plafond de diff.slice et ajoutez un outil qui récupère le patch d'un seul fichier à la demande. Laisser l'agent tirer les fichiers sélectivement vaut mieux que forcer tout le diff à travers la fenêtre.
Erreurs de migration depuis AI SDK 6. Lancez le codemod plutôt que d'éditer à la main :
npx @ai-sdk/codemod v7Le principal changement manuel est le passage d'Agent à ToolLoopAgent. Si votre agent utilisait une boucle écrite à la main autour de generateText, le codemod n'y touchera pas — cette migration-là vous revient, et elle mérite d'être faite délibérément.
Prochaines étapes
- Élargissez l'outillage. Ajoutez des outils pour l'état de la CI, les issues liées et les fils de revue précédents afin que l'agent raisonne sur l'historique, pas seulement sur le diff courant.
- Composez les harnais. Faites tourner Claude Code et Codex sur la même PR et confrontez leurs conclusions — un désaccord entre deux harnais est un signal fort d'ambiguïté réelle dans le code.
- Ajoutez des évaluations. Constituez un jeu de PR aux défauts connus et mesurez le taux de détection à mesure que vous affinez les skills. Notre tutoriel Promptfoo couvre l'outillage nécessaire.
- Persistez les résultats. Écrivez les revues en base pour suivre quelles catégories de défauts votre équipe livre le plus souvent.
- Étendez vers MCP. Exposez vos outils via le Model Context Protocol pour que les mêmes capacités fonctionnent dans Cursor et Claude Desktop — voir notre tutoriel serveur MCP.
Conclusion
Le changement apporté par AI SDK 7 est un changement d'altitude. Vous n'assemblez plus un agent à partir de primitives ; vous configurez un environnement pour un agent qui fonctionne déjà. Les pièces que vous fournissez sont celles que vous seul pouvez fournir : la frontière de la sandbox, la connaissance métier encodée en skill, les outils typés qui atteignent vos systèmes, et la politique d'approbation qui décide quelles actions une machine peut mener sans supervision.
Ce dernier point mérite qu'on s'y attarde. Un agent de code doté d'une isolation en sandbox, d'un cantonnement des identifiants via contextSchema et de garde-fous d'approbation sur chaque action visible de l'extérieur n'est pas une démo — c'est un système que vous pouvez pointer vers un vrai dépôt. Les couches que vous avez construites ici, dans cet ordre, sont exactement celles qui rendent cela vrai.
Commencez par le chemin en lecture seule. Amenez la qualité des revues là où vous la voulez. Et seulement ensuite, confiez à l'agent un token capable d'écrire.