Apple presentó FoundationModels en 2025 como una API nativa de Swift para trabajar con el modelo local de Apple Intelligence. La primera versión ya incluía sesiones con estado, generación estructurada mediante @Generable, streaming y llamadas a herramientas. Sin embargo, crear un asistente que permanezca activo durante mucho tiempo o que pueda cambiar de modelo plantea problemas que van más allá de enviar un prompt y recibir una respuesta.
Con iOS 27, Apple ha dado un paso más con FoundationModelsUtilities, un paquete de código abierto que reúne patrones emergentes para construir experiencias más complejas. No sustituye al framework principal ni pretende convertirse en una arquitectura completa para agentes. Su objetivo es proporcionar piezas reutilizables para resolver tres necesidades concretas:
- Conectar
LanguageModelSessiona servidores compatibles con el protocoloChat Completions. - Controlar el crecimiento del historial de una sesión.
- Cargar instrucciones especializadas únicamente cuando sean necesarias.
El paquete está publicado en GitHub y Apple lo describe expresamente como un espacio para explorar técnicas novedosas y experimentales. Esto permite que sus APIs evolucionen entre versiones del sistema operativo, aunque también obliga a asumir que pueden cambiar con mayor rapidez que las incluidas directamente en los SDK de Apple.
Añadir Foundation Models Utilities al proyecto
El repositorio puede añadirse desde File > Add Package Dependencies en Xcode utilizando esta URL:
https://github.com/apple/foundation-models-utilities
En un Package.swift, la dependencia se declara de esta forma:
let package = Package(
name: "HomeSupport",
dependencies: [
.package(
url: "https://github.com/apple/foundation-models-utilities",
from: "1.0.0"
)
],
targets: [
.target(
name: "HomeSupport",
dependencies: [
.product(
name: "FoundationModelsUtilities",
package: "foundation-models-utilities"
)
]
)
]
)
La versión actual del manifiesto utiliza Swift Tools 6.2 y el modo de lenguaje Swift 6. En plataformas Apple exige iOS 27, macOS 27, visionOS 27 o watchOS 27. El repositorio también contempla determinados entornos Linux, algo especialmente relevante ahora que Apple está abriendo el framework Foundation Models y su capa de abstracción de modelos.
Un mismo LanguageModelSession para modelos externos
Una de las novedades de Foundation Models en iOS 27 es el protocolo LanguageModel. Tanto el modelo local como Private Cloud Compute conforman este protocolo, por lo que ambos pueden utilizarse detrás de la misma API de sesión. FoundationModelsUtilities extiende esta idea con ChatCompletionsLanguageModel, una implementación capaz de comunicarse con un servidor que exponga una API compatible con Chat Completions.
import FoundationModels
import FoundationModelsUtilities
let model = ChatCompletionsLanguageModel(
name: "home-maintenance-model",
url: URL(string: "http://localhost:8080/v1")!,
supportsGuidedGeneration: true
)
let session = LanguageModelSession(model: model)
let response = try await session.respond(
to: "Resume los pasos para limpiar el filtro de una secadora."
)
print(response.content)
Lo importante de este diseño no es únicamente poder llamar a otro modelo, sino mantener la misma abstracción. La aplicación continúa trabajando con LanguageModelSession, sus transcripciones, sus herramientas y la generación guiada. Cambiar el motor que hay detrás no obliga a crear una capa de conversación completamente distinta.
El inicializador también admite cabeceras adicionales y una configuración de URLSession, lo que permite configurar autenticación, tiempos de espera o incluso un proxy:
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 45
let model = ChatCompletionsLanguageModel(
name: "home-maintenance-model",
url: URL(string: "https://models.example.com/v1")!,
additionalHeaders: [
"Authorization": "Bearer \(shortLivedAccessToken)"
],
supportsGuidedGeneration: false,
urlSessionConfiguration: configuration
)
Esta posibilidad no convierte automáticamente cualquier servidor en un reemplazo equivalente del modelo de Apple. Chat Completions describe un formato de intercambio, pero cada proveedor decide qué capacidades implementa realmente. El servidor debe ser compatible con las características que vaya a utilizar la sesión: respuestas en streaming, herramientas, imágenes, razonamiento o salida estructurada.
El parámetro supportsGuidedGeneration indica si el servidor acepta el campo response_format empleado para solicitar una respuesta estructurada. Si el backend no lo implementa, debe establecerse en false. Esta propiedad no corrige por sí sola otras diferencias del proveedor, por lo que conviene validar cada combinación de modelo y funcionalidad antes de llevarla a producción.
También hay una implicación importante de seguridad. Una clave privada incluida en el binario de una app puede extraerse, aunque se ofusque. Para servicios de pago resulta preferible obtener credenciales temporales mediante un flujo seguro o interponer un servidor propio que aplique autenticación, cuotas y controles de abuso.
El contexto no es una memoria infinita
LanguageModelSession conserva una transcripción con las instrucciones, los prompts, las respuestas, las llamadas a herramientas y los resultados devueltos por ellas. Este comportamiento facilita una conversación coherente, pero todo ese contenido ocupa espacio en la ventana de contexto del modelo.
Cuando la ventana se llena, no basta con eliminar unos cuantos mensajes de la interfaz. El modelo recibe la transcripción real, que también contiene información no visible para el usuario. Una respuesta extensa de una herramienta o unas instrucciones demasiado grandes pueden consumir más tokens que varios turnos de conversación.
No conviene codificar un límite fijo. En la presentación de iOS 27, Apple muestra un SystemLanguageModel con una ventana de 8192 tokens, pero el tamaño puede variar según el modelo y la plataforma. Desde las versiones recientes del framework es posible consultarlo y contar los tokens de un contenido:
let model = SystemLanguageModel()
print("Ventana disponible: \(model.contextSize) tokens")
let estimatedTokens = try await model.tokenCount(
for: "Comprueba el historial de mantenimiento del frigorífico."
)
print("Tokens del prompt: \(estimatedTokens)")
Esta información permite adaptar los umbrales a la capacidad real, reservar espacio para la respuesta y evitar que la aplicación falle justo cuando la conversación se vuelve más útil.
Eliminar llamadas a herramientas ya completadas
Las herramientas pueden añadir grandes cantidades de datos a la transcripción: resultados de búsqueda, documentos, identificadores o estructuras JSON. Después de que el modelo haya utilizado esa información para generar una respuesta, conservar todo el intercambio puede dejar de aportar valor.
El modificador droppingCompletedToolCalls() elimina las secuencias de herramientas ya completadas:
struct ApplianceProfile: LanguageModelSession.DynamicProfile {
var body: some DynamicProfile {
Profile {
Instructions(
"Ayuda a diagnosticar electrodomésticos sin inventar datos técnicos."
)
ReadDeviceManualTool()
}
.droppingCompletedToolCalls()
}
}
Es una estrategia eficaz cuando el resultado solo era necesario para producir la respuesta inmediata. No debe aplicarse a ciegas si el usuario puede referirse después a un dato concreto de esa herramienta, porque el modelo dejará de tener acceso a él. Si esa información es importante para la aplicación, debería extraerse y guardarse en un modelo de dominio antes de limpiar la transcripción.
Mantener una ventana deslizante
rollingWindow(entries:) conserva únicamente las entradas más recientes del historial:
struct ApplianceProfile: LanguageModelSession.DynamicProfile {
var body: some DynamicProfile {
Profile {
Instructions("Ofrece instrucciones breves y ordenadas.")
}
.rollingWindow(entries: 20)
}
}
Su principal ventaja es la previsibilidad: el historial nunca superará el número de entradas indicado. No obstante, una entrada no equivale a una cantidad estable de tokens. Un mensaje de una línea y la salida completa de un manual cuentan como una entrada cada uno, aunque ocupen tamaños muy distintos.
Por ese motivo, la ventana deslizante funciona bien como límite sencillo, pero no sustituye a una medición exacta de tokens cuando se necesita aprovechar el contexto con precisión.
Resumir la conversación anterior
Eliminar el contenido antiguo es “barato”, aunque también puede eliminar cualquier detalle incluido en él. summarizeHistory(entryThreshold:model:) ofrece un punto intermedio: cuando el historial supera el umbral, utiliza otro modelo para condensarlo en una entrada más pequeña.
struct ApplianceProfile: LanguageModelSession.DynamicProfile {
let summarizerModel: SystemLanguageModel
var body: some DynamicProfile {
Profile {
Instructions(
"Ayuda al usuario a resolver incidencias domésticas de forma segura."
)
ReadDeviceManualTool()
}
.summarizeHistory(
entryThreshold: 16,
model: summarizerModel
)
}
}
El modelo que resume no tiene por qué ser el mismo que produce la respuesta final. En una arquitectura con varios proveedores puede utilizarse un modelo rápido y económico para comprimir el contexto y reservar otro más capaz para la interacción con el usuario.
El resumen sigue siendo una generación probabilística. Puede omitir una condición, confundir una fecha o reducir un matiz que después resulte relevante. El estado esencial —por ejemplo, un identificador, una nombre ya utilizado o una operación pendiente— debe almacenarse en estructuras tipadas y persistentes. La transcripción ayuda al modelo a conversar; no debería ser la base de datos de la aplicación.
Combinar las estrategias y entender su orden
Los tres modificadores pueden componerse dentro del mismo perfil:
struct ApplianceProfile: LanguageModelSession.DynamicProfile {
let summarizerModel: SystemLanguageModel
var body: some DynamicProfile {
Profile {
Instructions(
"Ayuda a diagnosticar electrodomésticos y prioriza la seguridad."
)
ReadDeviceManualTool()
}
.summarizeHistory(
entryThreshold: 16,
model: summarizerModel
)
.rollingWindow(entries: 24)
.droppingCompletedToolCalls()
}
}
El orden de escritura puede resultar engañoso. Los modificadores se aplican de fuera hacia dentro: en este ejemplo primero se eliminan las llamadas a herramientas completadas, después se limita el historial a 24 entradas y finalmente se resume si todavía supera el umbral de 16.
Esta secuencia evita gastar tokens resumiendo información que iba a eliminarse de todos modos. También demuestra que no existe una configuración universal. Una generación aislada probablemente no necesite ninguna transformación, mientras que un asistente abierto durante horas puede necesitar las tres.
Skills: instrucciones bajo demanda
Otra fuente que consume contexto son las instrucciones. Una aplicación capaz de responder sobre muchos dominios podría incluir desde el primer turno todas sus reglas, manuales y herramientas, pero el modelo tendría que procesarlas incluso cuando no fueran relevantes.
Las Skill permiten registrar una descripción breve y cargar el contenido completo cuando el propio modelo decide que la tarea lo necesita. SkillActivations mantiene el conjunto de skills activas y, al conformar a Observable y RandomAccessCollection, también puede utilizarse para actualizar la interfaz en SwiftUI.
import Observation
@Observable
final class SupportRuntime {
var skillActivations = SkillActivations()
}
struct SupportProfile: LanguageModelSession.DynamicProfile {
let runtime: SupportRuntime
var body: some DynamicProfile {
Profile {
Instructions(
"Eres un asistente de soporte doméstico. Sé preciso y prudente."
)
Skills(activations: runtime.skillActivations) {
Skill(
name: "washer-manuals",
description: "Consulta convenciones y manuales de lavadoras",
prompt: washerReferenceMaterial
)
Skill(
name: "electrical-safety",
description: "Aplica reglas de seguridad para incidencias eléctricas",
instructions: electricalSafetyInstructions,
allowsDeactivation: true
)
}
}
}
}
Aunque ambas variantes se llaman Skill, no producen el mismo efecto.
Skills basadas en prompt
Una skill inicializada con prompt introduce su contenido como resultado de la llamada de activación. Las instrucciones originales de la sesión no cambian, por lo que el modelo puede reutilizar su caché de claves y valores. Esto reduce el trabajo necesario antes de empezar a generar la siguiente respuesta.
Esta variante encaja bien con material de referencia: documentación de producto, glosarios, convenciones de escritura o conocimiento específico que el modelo debe consultar, pero que no necesita la prioridad de una instrucción de sistema.
Skill(
name: "dryer-error-codes",
description: "Interpreta códigos de error de secadoras compatibles",
prompt: dryerErrorCodeReference
)
Skills basadas en instructions
Cuando se utiliza instructions, el contenido se añade a la primera entrada de instrucciones de la transcripción. El modelo suele otorgarle más prioridad, así que es la opción apropiada para reglas que deben dirigir su comportamiento durante una tarea.
Modificar las instrucciones puede invalidar la caché del prefijo. En consecuencia, el siguiente turno puede tardar más en producir el primer token porque el modelo necesita volver a procesarlas. Si se habilita allowsDeactivation, el modelo puede retirar la skill al terminar y liberar ese contexto para futuras peticiones.
Skill(
name: "gas-safety",
description: "Activa el protocolo para una posible fuga de gas",
instructions: """
Prioriza abandonar la vivienda y contactar con emergencias.
No sugieras accionar interruptores ni manipular la instalación.
""",
allowsDeactivation: true
)
La activación y desactivación dependen de decisiones del modelo, por lo que no son completamente deterministas. Una skill puede mejorar el comportamiento del asistente, pero nunca debe utilizarse como mecanismo de autorización ni como barrera única de seguridad. Las operaciones sensibles, los permisos y la validación de argumentos deben continuar implementándose en código convencional.
Skills de la aplicación y skills del repositorio
El repositorio contiene además una carpeta skills/ destinada a agentes de programación. Esos archivos enseñan a una herramienta de desarrollo cómo utilizar el paquete. No son las mismas skills que las instancias de Skill creadas dentro de una aplicación.
La idea general es parecida —cargar conocimiento especializado cuando hace falta—, pero el entorno y el ciclo de vida son distintos:
- Las skills del repositorio ayudan al agente que escribe el código.
- Las skills de
FoundationModelsUtilitiesmodifican el contexto de unaLanguageModelSessiondurante la ejecución de la app.
Confundir ambas capas puede llevar a esperar que un archivo de instrucciones incluido para el agente se distribuya o se active automáticamente dentro de la aplicación, algo que no sucede.
Cuándo merece la pena utilizar el paquete
Foundation Models Utilities resulta especialmente útil cuando una aplicación mantiene sesiones largas, usa muchas herramientas, necesita instrucciones especializadas o quiere conectar la API de Apple a un servidor compatible con Chat Completions.
Para una función pequeña que envía un único prompt al modelo local y muestra su respuesta, probablemente añade complejidad innecesaria. En ese caso, FoundationModels ya proporciona las piezas esenciales con menos dependencias y una superficie de API más estable.
En proyectos más ambiciosos, el paquete señala una dirección interesante para el ecosistema Swift: separar la sesión de la implementación concreta del modelo, tratar el contexto como un recurso limitado y componer capacidades bajo demanda. No resuelve por sí solo la privacidad, la seguridad ni la fiabilidad, pero sí ofrece abstracciones nativas para que esas decisiones formen parte explícita de la arquitectura.