AI SDK 6 establece para una aplicación TypeScript una frontera clara entre las decisiones del modelo y la autoridad de la aplicación. El modelo puede elegir una herramienta tipada, recibir su resultado y continuar la conversación. Tu código sigue validando entradas, aplicando permisos, decidiendo si un efecto secundario necesita a una persona y registrando lo ocurrido. Esa separación es el principio de diseño útil para un agente en producción. Este artículo usa las API estables de AI SDK 6, no la API de aprobaciones más reciente de AI SDK 7. Instala ai@6 junto con un paquete de proveedor compatible con la rama 6.x, como @ai-sdk/openai@3, y fija las versiones en el lockfile.
El bucle que estás construyendo
Una llamada única al modelo puede devolver texto o una llamada de herramienta. Un bucle de herramientas añade otra llamada al modelo después de ejecutar la herramienta. Así el modelo puede interpretar el resultado y decidir si hace falta otra herramienta. Cada generación del modelo es un paso. El bucle termina cuando el modelo deja de pedir herramientas, cuando la herramienta llamada no tiene execute, cuando se necesita aprobación o cuando se cumple stopWhen. El resultado expone el texto final, las llamadas y resultados de herramientas, los mensajes de respuesta, el uso y steps. El flujo queda visible y se puede probar.
ToolLoopAgent encapsula este comportamiento en un objeto reutilizable. Su constructor requiere un LanguageModel y acepta instructions, tools, stopWhen, output, prepareStep, maxRetries, tiempos de espera y callbacks. En AI SDK 6 la condición predeterminada es stepCountIs(20). Define un límite menor para un flujo acotado. El límite controla coste y disponibilidad, pero no sustituye la autorización.
Un ToolLoopAgent tipado
El ayudante tool infiere el tipo de los argumentos de execute desde inputSchema. El esquema se envía al proveedor y se usa para validar los argumentos que envía el modelo. El modelo no se vuelve confiable por ello. Una forma válida puede contener la cuenta equivocada, una ruta prohibida, una URL peligrosa o un importe no autorizado.
npm install [email protected] @ai-sdk/[email protected] @ai-sdk/[email protected] zod
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent, stepCountIs, tool } from 'ai';
import { z } from 'zod';
const getWeather = tool({
description: 'Get the current weather for a city',
inputSchema: z.object({
city: z.string().min(1).max(80),
}),
execute: async ({ city }) => ({
city,
temperatureC: 18,
condition: 'cloudy',
}),
});
const agent = new ToolLoopAgent({
model: openai('gpt-4o-mini'),
instructions: 'Use getWeather for current weather. Never invent a tool result.',
tools: { getWeather },
stopWhen: stepCountIs(4),
maxRetries: 2,
});
const result = await agent.generate({
prompt: 'What is the weather in Kyiv?',
});
console.log(result.text);
console.log(result.steps.length);
Una herramienta debe hacer una operación y devolver un resultado pequeño y serializable. La descripción debe precisar unidades, permisos, actualidad de los datos y comportamiento ante errores. Usa strict: true cuando el proveedor admita llamadas estrictas y el esquema sea compatible. El modo estricto aumenta la fiabilidad, pero no es autorización. toolChoice: 'auto' deja que el modelo decida, required exige una herramienta disponible, none desactiva las herramientas y { type: 'tool', toolName: 'getWeather' } elige una herramienta concreta. activeTools puede reducir las herramientas expuestas en un paso.
Llamadas explícitas y límites
Usa generateText para una llamada puntual o cuando necesites controlar directamente el historial de mensajes. Añade stopWhen si los resultados de las herramientas deben volver al modelo. Sin esa opción, generateText hace una generación y devuelve la llamada sin continuar automáticamente.
import { openai } from '@ai-sdk/openai';
import { generateText, stepCountIs, tool } from 'ai';
import { z } from 'zod';
const lookupOrder = tool({
description: 'Look up an order by its public order number',
inputSchema: z.object({ orderNumber: z.string().regex(/^ORD-[0-9]{6}$/) }),
execute: async ({ orderNumber }) => ({
orderNumber,
status: 'shipped',
}),
});
const result = await generateText({
model: openai('gpt-4o-mini'),
tools: { lookupOrder },
stopWhen: stepCountIs(3),
prompt: 'Check order ORD-104209 and explain its status.',
});
console.log(result.text);
console.log(result.steps.flatMap(step => step.toolCalls));
Las condiciones integradas son stepCountIs(count), hasToolCall(toolName) e isLoopFinished(). Un arreglo detiene el bucle cuando coincide cualquiera. isLoopFinished() no tiene máximo, así que úsalo solo con un presupuesto externo, tiempo de espera, cancelación y cuota del proveedor. Un StopCondition personalizado recibe { steps } y puede detenerse por un estado de negocio o un presupuesto medido de tokens. Las condiciones se evalúan cuando el último paso contiene resultados de herramientas. Si combinas llamadas con salida estructurada, reserva un paso adicional para generar la salida.
maxRetries repite las llamadas de modelo fallidas. No hace idempotente a execute. Una herramienta que envía correo, cobra una tarjeta o crea un registro debe usar una clave de idempotencia y detectar duplicados por su cuenta. En un cliente MCP remoto, los reintentos están desactivados de forma predeterminada y se configuran con maxRetries en createMCPClient. Repite solo errores de red o de límite de velocidad. No repitas a ciegas una solicitud tools/call no idempotente.
Salida estructurada utilizable desde TypeScript
AI SDK 6 marca generateObject y streamObject como obsoletos y recomienda generateText y streamText con output. Output.object acepta un esquema Zod, Valibot o JSON Schema. La respuesta completa se analiza y valida antes de resolver result.output. Los objetos parciales sirven para la interfaz, pero no son una validación final.
import { openai } from '@ai-sdk/openai';
import { generateText, Output } from 'ai';
import { z } from 'zod';
const reportSchema = z.object({
sentiment: z.enum(['positive', 'neutral', 'negative']),
score: z.number().min(0).max(1),
keyPoints: z.array(z.string().min(1)).max(8),
});
const { output } = await generateText({
model: openai('gpt-4o-mini'),
output: Output.object({
schema: reportSchema,
name: 'review_report',
description: 'A concise, evidence-based review report',
}),
prompt: 'Analyze: The battery lasts all day, but the charger is bulky.',
});
console.log(output.sentiment, output.score, output.keyPoints);
La validación tiene dos niveles. El proveedor puede imponer el formato, y AI SDK analiza el JSON recibido y lo valida con el esquema. La aplicación todavía debe comprobar reglas de negocio, como si un identificador pertenece al tenant actual o si una puntuación puede activar un reembolso. Mantén los esquemas cerrados y precisos. Limita cadenas, arreglos y números, y usa enumeraciones. Rechaza comandos desconocidos en lugar de enviar JSON arbitrario a un adaptador privilegiado.
Con herramientas, el modelo puede hacer primero una búsqueda y después producir el informe. Define stopWhen: stepCountIs(4) u otro presupuesto explícito, porque el paso de salida estructurada forma parte del mismo flujo de varios pasos. Si falla el análisis, captura el error del SDK, conserva el identificador de correlación y los metadatos del proveedor, y devuelve una respuesta segura que se pueda reintentar. No pidas a otro modelo que autorice una salida malformada.
Streaming de texto y eventos
streamText ofrece flujos asíncronos para texto y salida estructurada. ToolLoopAgent.stream devuelve un StreamTextResult después de preparar la llamada, por lo que debes esperar el resultado antes de leer textStream. El flujo de texto contiene el texto generado, mientras que el resultado completo y los callbacks exponen llamadas y resultados de herramientas.
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: openai('gpt-4o-mini'),
instructions: 'Answer clearly and briefly.',
});
const stream = await agent.stream({
prompt: 'Explain why typed tool inputs matter.',
});
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}
Para un flujo estructurado usa streamText con Output.object y consume partialOutputStream. onStepFinish permite guardar un paso terminado, registrar uso y mostrar un evento de auditoría. El flujo puede terminar con tool-approval-request, tool-error o tool-output-denied, no solo con texto. Un cliente que solo muestra texto puede ocultar una acción que espera a una persona.
Aprobación humana de efectos secundarios
En AI SDK 6 define needsApproval en la herramienta, con true o con un predicado asíncrono basado en una entrada validada. La primera llamada a generateText o streamText devuelve una parte tool-approval-request. El servidor no se pausa esperando al navegador. Guarda los mensajes de respuesta, muestra el nombre exacto de la herramienta y sus argumentos, añade un tool-approval-response a un nuevo mensaje tool y vuelve a llamar al modelo.
Este ejemplo completo para Node usa una pregunta de terminal como frontera humana. Una aplicación web guardaría messages, approvalId, toolCallId, el revisor autenticado y la caducidad en un almacén del servidor.
import { createInterface } from 'node:readline/promises';
import { openai } from '@ai-sdk/openai';
import {
generateText,
tool,
type ModelMessage,
type ToolApprovalResponse,
} from 'ai';
import { z } from 'zod';
const records = new Map([
['draft-17', { ownerId: 'user-7', text: 'Quarterly notes' }],
]);
const deleteDraft = tool({
description: 'Delete one draft owned by the authenticated user',
inputSchema: z.object({ draftId: z.string().regex(/^draft-[0-9]+$/) }),
needsApproval: true,
execute: async ({ draftId }) => {
if (!records.delete(draftId)) {
throw new Error('Draft was not found');
}
return { draftId, deleted: true };
},
});
async function requestHumanApproval(toolName: string, input: unknown) {
const terminal = createInterface({ input: process.stdin, output: process.stdout });
const answer = await terminal.question(`Approve ${toolName} ${JSON.stringify(input)}? [y/N] `);
terminal.close();
return answer.trim().toLowerCase() === 'y';
}
const messages: ModelMessage[] = [
{ role: 'user', content: 'Delete draft-17.' },
];
const first = await generateText({
model: openai('gpt-4o-mini'),
system: 'If an action is denied, do not retry it.',
tools: { deleteDraft },
messages,
});
messages.push(...first.response.messages);
const approvals: ToolApprovalResponse[] = [];
for (const part of first.content) {
if (part.type === 'tool-approval-request') {
approvals.push({
type: 'tool-approval-response',
approvalId: part.approvalId,
approved: await requestHumanApproval(part.toolCall.toolName, part.toolCall.input),
reason: 'Decision made by the authenticated reviewer',
});
}
}
if (approvals.length > 0) {
messages.push({ role: 'tool', content: approvals });
const final = await generateText({
model: openai('gpt-4o-mini'),
system: 'If an action is denied, do not retry it.',
tools: { deleteDraft },
messages,
});
console.log(final.text);
} else {
console.log(first.text);
}
La aprobación no sustituye a la autorización. Justo antes del efecto secundario, vuelve a comprobar usuario, tenant, objetivo, política y versión del recurso. Vincula la aprobación guardada con approvalId, toolCallId, el nombre de la herramienta y un hash de la entrada validada. Ponle caducidad, permite un solo uso y rechaza una entrada modificada. La denegación debe convertirse en un resultado claro y no repetirse automáticamente. Para una acción de alto impacto muestra objetivo, identidad, argumentos, datos que salen del sistema y efectos esperados, no solo el resumen del modelo.
Herramientas peligrosas y límites de seguridad
No expongas al modelo un shell general, un cliente HTTP sin restricciones, una ruta de archivo arbitraria ni una conexión directa a la base de datos. Prefiere operaciones estrechas como deleteDraft, createCalendarEvent o lookupOrder. Coloca la autorización y las listas permitidas en execute o en un servicio de políticas. Valida esquema de URL, host, puerto, dirección resuelta y redirecciones. Resuelve rutas dentro de una raíz permitida y considera enlaces simbólicos. Comprueba la propiedad del tenant en la consulta de base de datos, no solo en el prompt.
Trata las descripciones de herramientas, documentos recuperados, memoria y resultados como contenido no confiable. Un prompt injection puede pedir al modelo revelar un secreto, llamar una herramienta ajena o enviar datos a un sitio controlado por un atacante. El modelo no es una frontera de seguridad. Los controles de mínimo privilegio, procedencia, salida de red, aislamiento, límites y aprobaciones se explican en Prompt injection y seguridad MCP. Para separar planificador, ejecutor, políticas y almacenamiento consulta la arquitectura de un agente de IA en producción.
Nunca pongas claves API en prompts ni resultados. Redacta tokens en logs y telemetría. No registres la entrada completa si puede contener datos personales. Propaga un identificador de solicitud por las llamadas del modelo y las herramientas. Limita el tamaño de un resultado antes de añadirlo al siguiente prompt. Usa credenciales distintas para lectura, borrador y confirmación. Ejecuta un agente de navegador o archivos en un worker aislado, sin secretos ajenos y con red denegada por defecto.
MCP con tipado seguro
El paquete @ai-sdk/mcp adapta herramientas de un servidor MCP a herramientas de AI SDK. AI SDK 6 recomienda HTTP en producción y reserva stdio para servidores locales. Define esquemas explícitos cuando no controles el servidor o la herramienta sea sensible. Así mantienes un conjunto reducido y tipos útiles de entrada en TypeScript. Usa redirect: 'error' si la política no permite redirecciones, valida los orígenes de servidores de autorización OAuth y cierra el cliente en finally o onFinish.
import { openai } from '@ai-sdk/openai';
import { createMCPClient } from '@ai-sdk/mcp';
import { ToolLoopAgent, stepCountIs } from 'ai';
import { z } from 'zod';
const mcpUrl = process.env.MCP_URL;
if (!mcpUrl) throw new Error('MCP_URL is required');
const mcpClient = await createMCPClient({
transport: {
type: 'http',
url: mcpUrl,
headers: process.env.MCP_TOKEN
? { Authorization: `Bearer ${process.env.MCP_TOKEN}` }
: undefined,
redirect: 'error',
},
maxRetries: 2,
});
try {
const tools = await mcpClient.tools({
schemas: {
'get-customer-note': {
inputSchema: z.object({ customerId: z.string().uuid() }),
},
},
});
const agent = new ToolLoopAgent({
model: openai('gpt-4o-mini'),
instructions: 'Use only the customer note tool for this request.',
tools,
stopWhen: stepCountIs(3),
});
const result = await agent.generate({ prompt: 'Read the requested customer note.' });
console.log(result.text);
} finally {
await mcpClient.close();
}
El descubrimiento automático mediante mcpClient.tools() es cómodo, pero expone cada herramienta anunciada y no ofrece tipos de entrada en compilación. Los esquemas explícitos cargan solo las herramientas nombradas. Reintenta únicamente errores de red transitorios. Los errores de aplicación MCP y las respuestas exitosas con isError: true deben llegar sin repetir efectos secundarios. OAuth todavía exige comprobación de audiencia, PKCE, tokens de corta duración, URI de redirección exactas y una lista de orígenes autorizados. La parte del servidor se muestra en Servidor MCP TypeScript con OAuth.
Errores, pruebas y operación
Rodea la generación con una frontera que distinga entrada inválida, fallo del proveedor, tiempo agotado, cancelación, salida malformada y error de herramienta. AI SDK convierte una excepción de execute en una parte tool-error, para que el modelo de varios pasos pueda ver el fallo. Devuelve un mensaje saneado desde la herramienta o transforma el resultado antes de entregarlo al modelo. No expongas trazas, secretos, SQL, rutas locales ni cuerpos de respuesta upstream. Limita cada solicitud con abortSignal y timeout. Registra finishReason, usage, totalUsage, número de pasos y decisión de política.
AI SDK 6 incluye mocks deterministas en ai/test. MockLanguageModelV3 permite devolver una llamada de herramienta en la primera generación y texto en la segunda. La prueba siguiente demuestra que el bucle ejecuta la herramienta una vez y obtiene la respuesta final sin contactar al proveedor.
import assert from 'node:assert/strict';
import { generateText, stepCountIs, tool } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
import { z } from 'zod';
const usage = {
inputTokens: { total: 1, noCache: 1, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: 1, text: 1, reasoning: undefined },
};
let calls = 0;
let executions = 0;
const model = new MockLanguageModelV3({
doGenerate: async () => {
calls += 1;
if (calls === 1) {
return {
content: [{ type: 'tool-call', toolCallId: 'call-1', toolName: 'add', input: '{"a":2,"b":3}' }],
finishReason: { unified: 'tool-calls', raw: undefined },
usage,
warnings: [],
};
}
return {
content: [{ type: 'text', text: 'The result is 5.' }],
finishReason: { unified: 'stop', raw: undefined },
usage,
warnings: [],
};
},
});
const add = tool({
description: 'Add two numbers',
inputSchema: z.object({ a: z.number(), b: z.number() }),
execute: async ({ a, b }) => {
executions += 1;
return a + b;
},
});
const result = await generateText({
model,
tools: { add },
stopWhen: stepCountIs(2),
prompt: 'Add 2 and 3.',
});
assert.equal(result.text, 'The result is 5.');
assert.equal(calls, 2);
assert.equal(executions, 1);
Añade pruebas para esquemas rechazados, tenants incorrectos, traversal de rutas, direcciones SSRF, claves de idempotencia duplicadas, caducidad de aprobación, denegación, listas permitidas de MCP y validación de salida. Comprueba las partes de herramienta y aprobación del flujo, no solo el texto visible. Ejecuta casos adversariales con documentos que ordenen ignorar la tarea o filtrar el contexto. La guía de evaluación de agentes de IA cubre conjuntos de regresión, aserciones de llamadas y mediciones de coste y latencia.
Notas de migración desde AI SDK 5
Sustituye Experimental_Agent por ToolLoopAgent. Su opción system pasa a llamarse instructions. La condición predeterminada cambia de stepCountIs(1) a stepCountIs(20), por lo que una actualización puede generar más llamadas. Define un límite explícito. Sustituye generateObject y streamObject por generateText y streamText con Output.object, Output.array u otra estrategia. El resultado en streaming usa partialOutputStream.
CoreMessage pasa a ser ModelMessage, y convertToModelMessages es asíncrona en AI SDK 6. ToolCallOptions pasa a ToolExecutionOptions. Los mocks V2 se convierten en MockLanguageModelV3 y los demás mocks V3 de ai/test. La opción de proveedor structuredOutputs se elimina de los modelos de chat en favor de strictJsonSchema. Para toModelOutput, desestructura el argumento { output } como exige la firma v6. Ejecuta el codemod de v6 y revisa manualmente adaptadores, conversión de mensajes, repetición de aprobaciones y pruebas de streaming. Un codemod renombra símbolos, pero no decide si el nuevo valor predeterminado es seguro para tu flujo.
La superficie exacta depende del patch fijado y del proveedor. Antes de actualizar, lee la referencia ToolLoopAgent, la guía de llamadas de herramientas, la guía de datos estructurados, la guía MCP, la guía de pruebas y la guía de migración de AI SDK 5 a 6. Trata como fallo de prueba cualquier advertencia que indique que una opción fue ignorada.