Build agentic app experiences with the Foundation Models framework
Use DynamicProfile, DynamicInstructions, history transforms, lifecycle modifiers, and tool orchestration to build agentic Foundation Models app flows.
TL;DR
- DynamicProfile lets a LanguageModelSession switch instructions, tools, model, temperature, reasoningLevel, and other options per app phase or agent.
- DynamicInstructions, historyTransform, custom modifiers, lifecycle modifiers, and session properties support reusable context engineering and transcript management.
- The session covers baton-pass and phone-a-friend orchestration patterns, plus Skills from the Foundation Models framework utilities package.
- ToolCallingMode, transcriptErrorHandlingPolicy, mutable transcripts, KV cache behavior, Instruments, and Evaluations are key when building advanced agentic workflows.
Dynamic profiles as agent configuration
DynamicProfile is a new Foundation Models framework API for changing the active configuration of a LanguageModelSession. A profile can represent an app phase or agent, with its own instructions, tools, model, sampling options, temperature, reasoningLevel, and other modifiers.
The sample Origami craft app has brainstorming, planning, and reviewing phases. They share context, but each phase has different needs: brainstorming favors creativity, planning benefits from deeper reasoning, and reviewing can use the on-device SystemLanguageModel to avoid unnecessary server calls. The body of a DynamicProfile is re-evaluated on each prompt, so changing app state can swap the session persona.
- Use PrivateCloudComputeLanguageModel where server model capabilities are needed, such as broad craft knowledge or deep tutorial planning.
- Use SystemLanguageModel where an on-device model is sufficient, such as reviewing in-progress craft photos and giving technique advice.
- Initialize a session directly with a dynamic profile using the new LanguageModelSession initializer.
Define per-phase model configuration
A single session can route between distinct agents by re-evaluating the profile from app state.
struct CraftProfile: LanguageModelSession.DynamicProfile {
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .brainstorming:
Profile {
BrainstormFacilitator(orchestrator: orchestrator)
}
.model(orchestrator.pccLanguageModel)
.temperature(1)
case .planning:
Profile {
TutorialAuthor(orchestrator: orchestrator)
}
.model(orchestrator.pccLanguageModel)
.reasoningLevel(.deep)
case .reviewing:
Profile {
CraftCoach()
}
.model(orchestrator.systemLanguageModel)
}
}
}
let session = LanguageModelSession(profile: CraftProfile(orchestrator: orchestrator))Reusable dynamic instructions and utilities
DynamicInstructions groups related instructions and tools into a reusable component. Nesting one DynamicInstructions component inside another concatenates its instructions and tools, which makes domain packages such as an OrigamiExpert reusable across profiles.
Apple also introduced the open-source Foundation Models framework utilities package. It contains components for agentic experiences, including history-management modifiers and the Skills pattern for procedural context loading, and can evolve between OS releases.
- Use DynamicInstructions for reusable instruction/tool bundles.
- Conditionally include domain expertise or tools based on app state, such as adding origami-specific support only for origami projects.
- Use Skills from the utilities package when procedural context should be activated selectively.
Compose instructions and tools
DynamicInstructions can package instructions, tools, and conditional capabilities for reuse across profile declarations.
struct BrainstormFacilitator: DynamicInstructions {
var orchestrator: CraftOrchestrator
var body: some DynamicInstructions {
Instructions {
"You are a warm and friendly expert crafting brainstorm facilitator."
}
GenerateProjectTitle()
if orchestrator.techniques.contains(.origami) {
OrigamiExpert()
}
}
}Represent procedural context as skills
The Skills utility is presented as a pattern for procedural context loading.
struct CraftingSkills: LanguageModelSession.DynamicInstructions {
var activations: SkillActivations
var body: some DynamicInstructions {
Skills(activations: activations) {
Skill(
name: "origami_folds",
description: "Details about specific types of folds",
prompt: """
Valley Fold: Paper is folded toward you, creating a V-shaped crease
Mountain Fold: Paper is folded away from you, creating an inverted V
...
"""
)
Skill(...)
Skill(...)
}
}
}Transcript management and shared session state
A session transcript is the model's context. Dynamic profiles make it practical to trim, summarize, or redact context when switching between models with different context limits, privacy boundaries, or focus requirements.
historyTransform applies a stateless, per-request transform over the session history before prompting the model. It does not permanently mutate the session transcript, so it is preferred for lossless, profile-specific context shaping. For stateful changes shared by all profiles, use session properties such as the built-in history property.
- Use historyTransform to drop irrelevant entries, completed tool calls, or private content for a specific profile.
- Use DynamicProfileModifier to package transforms as reusable custom modifiers.
- Use lifecycle modifiers such as onResponse to run imperative code at response boundaries.
- Use @SessionPropertyEntry for custom state shared across profiles and tools, such as a conversation summary.
- Be careful: the built-in history session property is lossy and changes apply across all profiles.
Drop completed tool calls with a custom profile modifier
Custom modifiers hide transcript-transform complexity and make profile configuration reusable.
struct DroppingToolCallsProfileModifier: LanguageModelSession.DynamicProfileModifier {
func body(content: Content) -> some DynamicProfile {
content
.historyTransform { history in
guard let latestResponseIndex = lastResponseEntryIndex(history) else {
return history
}
let filteredHistory = history[0..<latestResponseIndex].filter { entry in
isToolCallsOrToolOutput(entry)
}
return filteredHistory + history[latestResponseIndex...]
}
}
}
extension LanguageModelSession.DynamicProfile {
func droppingCompletedToolCalls() -> some DynamicProfile {
self.modifier(DroppingToolCallsProfileModifier())
}
}Store a shared summary in a session property
A lifecycle modifier can summarize earlier transcript entries, store the summary, and trim shared history after a response.
extension SessionPropertyValues {
@SessionPropertyEntry var summary: String?
}
struct CraftProfile: LanguageModelSession.DynamicProfile {
@SessionProperty(\.history) var history
@SessionProperty(\.summary) var summary
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .planning:
Profile {
TutorialAuthor(orchestrator: orchestrator)
if let summary {
Instructions { "Summary: \(summary)" }
}
}
.onResponse {
if history.count > 50, let responseIndex = lastResponse(history.prefix(40)) {
summary = try await summarize(history[0..<responseIndex])
history = history[responseIndex...]
}
}
}
}
}Agent orchestration patterns
The session describes two common orchestration patterns: baton-pass and phone-a-friend. Both rely on tool calling, but they differ in transcript visibility and who produces the final answer.
In baton-pass, multiple profiles share the full transcript. A tool changes the active profile variable, and the receiving profile carries the request to completion. In phone-a-friend, a tool creates a short-lived child session with an independent transcript; the child returns tool output, and the parent profile always produces the final response.
- Use baton-pass for collaboration between profiles that should see the same full session history.
- Use phone-a-friend for consultation where an isolated specialist session should provide intermediate output.
- Represent handoffs and consultations as tools so the model can decide when to invoke them.
Baton-pass profile switch
A tool call changes the active profile, handing control to another agent that shares the transcript.
struct CraftProfile: LanguageModelSession.DynamicProfile {
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .brainstorm:
Profile {
BrainstormInstructions()
BatonPassTool()
}
.onToolCall { orchestrator.mode = .tutorial }
.model(orchestrator.serverModel)
case .tutorial:
Profile {
TutorialInstructions()
BatonPassTool()
}
.onToolCall { orchestrator.mode = .brainstorm }
.model(orchestrator.systemModel)
}
}
}Phone-a-friend child session
A tool can spawn a short-lived specialist session and return its response as tool output to the parent session.
struct PhoneFriendTool<P: LanguageModelSession.DynamicProfile>: Tool {
func call(arguments: GeneratedContent) async throws -> String {
let session = LanguageModelSession(profile: profile())
let response = try await session.respond(to: arguments)
return response.content
}
}Tool calling mode and transcript errors
ToolCallingMode controls whether a model may, must, or must not call tools. The default allowed mode preserves existing behavior. disallowed prevents tool use, and required forces the model to produce tool calls, which is useful for systems that represent actions as tools.
When tool calling is required, the model is effectively in a loop. The app must provide an exit condition, such as changing the tool calling mode after a specific tool call or using a final-answer tool that throws to abort the loop and return control to the app.
By default, a thrown tool error or cancellation rolls the transcript back. transcriptErrorHandlingPolicy can be set to .revertTranscript or .preserveTranscript. If preserving the transcript after an error, the app is responsible for restoring it to a usable state.
- Set tool calling mode as a profile modifier or through GenerationOptions when calling respond(to:).
- Use .required only with a clear exit path.
- Set transcriptErrorHandlingPolicy on a profile or directly on LanguageModelSession.
- LanguageModelSession.transcript is mutable, but only modify it when isResponding is false.
Require tools for a profile or a single generation
Tool calling mode can be part of a profile or a per-request generation option.
public struct ToolCallingMode: Sendable {
public static let allowed: ToolCallingMode
public static let disallowed: ToolCallingMode
public static let required: ToolCallingMode
}
struct OrigamiExpert: LanguageModelSession.DynamicProfile {
var body: some LanguageModelSession.DynamicProfile {
Profile {
Instructions("You are an origami expert")
QueryOrigamiDatabaseTool()
ShowDirectionsTool()
}
.toolCallingMode(.required)
}
}
let response = try await session.respond(
to: "Write out the instructions for folding a paper crane.",
options: GenerationOptions(toolCallingMode: .required)
)Preserve transcript after errors
Preserving the transcript supports advanced cancellation/resume flows, but requires manual cleanup if the transcript is left in a bad state.
struct OrigamiExpert: LanguageModelSession.DynamicProfile {
let state: OrigamiAppState
var body: some LanguageModelSession.DynamicProfile {
Profile {
Instructions("Answer questions about how to fold origami")
QueryOrigamiDatabaseTool()
}
.transcriptErrorHandlingPolicy(.preserveTranscript)
}
}
let session = LanguageModelSession()
session.transcriptErrorHandlingPolicy = .preserveTranscript
extension LanguageModelSession {
public struct TranscriptErrorHandlingPolicy: Sendable {
public static let revertTranscript: TranscriptErrorHandlingPolicy
public static let preserveTranscript: TranscriptErrorHandlingPolicy
}
}Performance, accuracy, and evaluation
Advanced transcript mutation affects both latency and model quality. KV caches are an important optimization for large language models. Appending to the transcript generally preserves cache state and minimizes time-to-first-token, while removing entries, changing attached tools, or updating instructions can invalidate caches and increase latency.
Rewriting history can also confuse the model. For example, adding a title-generation tool after earlier title-generation turns may cause the model to infer that direct generation is still acceptable. Context engineering changes should be measured rather than assumed.
- Use the upgraded Foundation Models Instrument in Xcode to detect cache invalidations and measure latency.
- Expect different models to have different caching behavior; measure the specific configuration.
- Use the Evaluations framework to build eval sets and quantify accuracy effects of transcript and context strategies.
- Prefer append-only behavior when possible, and mutate transcript deliberately when the benefit outweighs performance and accuracy risk.
Resources
What’s new in the Foundation Models framework
Foundation Models gains Private Cloud Compute, vision, model-provider abstraction, Dynamic Profiles, evaluations, CLI/Python tooling, and open source utilities.
Debug and profile agentic app experiences with Instruments
Use the enhanced Foundation Models instrument in Xcode 27 to trace prompts, tool calls, instruction handoffs, and latency in agentic LLM app flows.