Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,48 @@ session.toolExecutionDelegate = ToolExecutionObserver()
> Tool execution delegates are an AnyLanguageModel extension.
> See [Differences from Foundation Models](#differences-from-foundation-models).

### Dynamic Instructions

`DynamicInstructions` lets a session change the instructions and tools
for each request to the model,
without creating a new session:

```swift
final class CurrentAppState {
var canCheckWeather = false
}

struct CurrentAppInstructions: DynamicInstructions {
let state: CurrentAppState

var body: some DynamicInstructions {
Instructions("Help with the currently visible app.")
if state.canCheckWeather {
WeatherTool()
}
}
}

let state = CurrentAppState()
let session = LanguageModelSession(
model: model,
dynamicInstructions: CurrentAppInstructions(state: state),
history: savedHistory
)
```

The session evaluates the body before every request to the model,
including the request that continues a response after tool calls.
The resolved instructions are sent with each request
but never become part of the session's transcript.

> [!NOTE]
> Dynamic instructions follow the Foundation Models 27 API.
> `SystemLanguageModel` supports them only in apps built with Swift 6.4 or later
> that run on OS 27 or later, and not on tvOS.
> Otherwise, it throws `SystemLanguageModel.Error.dynamicInstructionsUnavailable`
> for a session with dynamic instructions.

### Reasoning in the transcript

Reasoning is transcript content, separate from the answer in `response.content`.
Expand Down Expand Up @@ -582,6 +624,9 @@ say which API they follow.
- `Usage` and the `usage` properties:
[token usage](#token-usage),
which follows the Foundation Models 27 API.
- `DynamicInstructions`, its builder, and `LanguageModelSession.init(model:dynamicInstructions:history:)`:
[dynamic instructions](#dynamic-instructions),
which follow the Foundation Models 27 API.
- `Transcript.Entry.reasoning` and `Transcript.Reasoning`:
[reasoning in the transcript](#reasoning-in-the-transcript),
which follows the Foundation Models 27 API.
Expand All @@ -590,7 +635,8 @@ say which API they follow.
for language models defined outside AnyLanguageModel.
- `Codable` conformance for `GeneratedContent`, `GenerationID`, `Usage`,
and the types nested in `Transcript`.
- `GeneratedContentError`, `Transcript.ReasoningReplayError`, and each provider's error type.
- `GeneratedContentError`, `Transcript.ReasoningReplayError`, `SystemLanguageModel.Error`,
and each provider's error type.
- `JSONValue`:
JSON values for provider options such as `extraBody`.
- Smaller additions to existing types,
Expand Down
79 changes: 77 additions & 2 deletions Sources/AnyLanguageModel/LanguageModelSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public final class LanguageModelSession: @unchecked Sendable {

/// The tools that the model can call during the session.
///
/// For a session created with dynamic instructions, this is empty.
/// Call ``resolvedRequestContext()`` to get the tools for the next request.
///
/// - Note: This property is exclusive to AnyLanguageModel
/// and using it means your code is no longer drop-in compatible
/// with the Foundation Models framework.
Expand All @@ -94,13 +97,24 @@ public final class LanguageModelSession: @unchecked Sendable {

/// The instructions for the session, if any.
///
/// For a session created with dynamic instructions, this is `nil`.
/// Call ``resolvedRequestContext()`` to get the instructions for the next request.
///
/// - Note: This property is exclusive to AnyLanguageModel
/// and using it means your code is no longer drop-in compatible
/// with the Foundation Models framework.
/// It's public so that language models outside this module
/// can read the session's instructions.
public let instructions: Instructions?

private let dynamicInstructions: AnyDynamicInstructions?
private let dynamicInstructionsLock = NSLock()

/// Whether the session resolves its instructions and tools before each request.
nonisolated var usesDynamicInstructions: Bool {
dynamicInstructions != nil
}

/// A delegate that observes and controls tool execution.
///
/// Set this property to intercept tool calls, provide custom output,
Expand Down Expand Up @@ -151,7 +165,29 @@ public final class LanguageModelSession: @unchecked Sendable {
/// It's public so that language models outside this module
/// can read the inputs for each request.
nonisolated public func resolvedRequestContext() -> RequestContext {
RequestContext(transcript: transcript, instructions: instructions, tools: tools)
guard let dynamicInstructions else {
return RequestContext(transcript: transcript, instructions: instructions, tools: tools)
}

// Evaluate the body for this request.
// The resolved instructions go into the request's transcript,
// never into the session's.
let resolved = dynamicInstructionsLock.withLock {
dynamicInstructions.resolveForRequest()
}
var requestTranscript = transcript
if let instructions = resolved.instructions {
let instructionsEntry = Transcript.Entry.instructions(
Transcript.Instructions(
segments: [.text(Transcript.TextSegment(content: instructions.description))],
toolDefinitions: resolved.tools
.filter(\.includesSchemaInInstructions)
.map { Transcript.ToolDefinition(tool: $0) }
)
)
requestTranscript = Transcript(entries: [instructionsEntry] + requestTranscript)
}
return RequestContext(transcript: requestTranscript, instructions: resolved.instructions, tools: resolved.tools)
}

/// Creates a session with a model, tools,
Expand Down Expand Up @@ -213,14 +249,53 @@ public final class LanguageModelSession: @unchecked Sendable {
self.init(model: model, tools: tools, instructions: nil, transcript: transcript)
}

/// Creates a session whose instructions and tools are resolved before each model request.
///
/// The session evaluates the body of `dynamicInstructions`
/// before every request to the model,
/// including the request that continues a response after tool calls.
/// The resolved instructions are sent with each request
/// but never become part of the session's transcript.
///
/// - Parameters:
/// - model: The language model to use.
/// - dynamicInstructions: The instructions and tools to resolve for each request.
/// - history: Earlier transcript entries to continue from.
/// Instructions entries in the history are left out,
/// because the dynamic instructions take their place.
///
/// - Note: This API is exclusive to AnyLanguageModel on OS 26.
/// It follows the Foundation Models 27 `LanguageModelSession.init(model:dynamicInstructions:history:)` API,
/// so code that uses it ports to Foundation Models on OS 27.
/// Unlike Foundation Models, AnyLanguageModel requires the `model` argument.
public convenience init(
model: any LanguageModel,
dynamicInstructions: sending some DynamicInstructions,
history: some Collection<Transcript.Entry> = []
) {
let history = history.filter { entry in
if case .instructions = entry { return false }
return true
}
self.init(
model: model,
tools: [],
instructions: nil,
transcript: Transcript(entries: history),
dynamicInstructions: AnyDynamicInstructions(dynamicInstructions)
)
}

private init(
model: any LanguageModel,
tools: [any Tool],
instructions: Instructions?,
transcript: Transcript
transcript: Transcript,
dynamicInstructions: AnyDynamicInstructions? = nil
) {
self.model = model
self.tools = tools
self.dynamicInstructions = dynamicInstructions
let resolvedInstructions = instructions ?? Self.instructions(from: transcript)
self.instructions = resolvedInstructions

Expand Down
73 changes: 73 additions & 0 deletions Sources/AnyLanguageModel/Models/SystemLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,21 @@
for session: LanguageModelSession,
prompt: Prompt
) throws -> FoundationModels.LanguageModelSession {
#if compiler(>=6.4) && !os(tvOS)
if #available(macOS 27.0, iOS 27.0, visionOS 27.0, watchOS 27.0, *) {
return makeFoundationModelsSession(
model: systemModel,
session: session,
prompt: prompt
)
}
#endif

// Before OS 27, Foundation Models can't resolve instructions and tools
// again for the request that continues after tool calls.
guard !session.usesDynamicInstructions else {
throw SystemLanguageModel.Error.dynamicInstructionsUnavailable
Comment on lines +311 to +312
}
let requestContext = session.resolvedRequestContext()
return FoundationModels.LanguageModelSession(
model: systemModel,
Expand Down Expand Up @@ -331,13 +346,71 @@
return Transcript(entries: transcript.dropLast())
}

@available(macOS 26.0, iOS 26.0, tvOS 26.0, visionOS 26.0, *)
@available(watchOS, unavailable)
extension SystemLanguageModel {
/// An error from the system language model.
///
/// - Note: This API is exclusive to AnyLanguageModel
/// and using it means your code is no longer drop-in compatible
/// with the Foundation Models framework.
/// Foundation Models 27 has a `SystemLanguageModel.Error` type with other cases.
public enum Error: LocalizedError, Sendable, Equatable {
/// The session uses dynamic instructions,
/// which the system language model supports only in apps built with Swift 6.4 or later
/// that run on OS 27 or later, and not on tvOS.
case dynamicInstructionsUnavailable

public var errorDescription: String? {
switch self {
case .dynamicInstructionsUnavailable:
"Dynamic instructions require an app built with Swift 6.4 or later that runs Foundation Models on OS 27 or later, and aren't available on tvOS."
}
}
}
}

#if compiler(>=6.4) && !os(tvOS)
/// Dynamic instructions that resolve an AnyLanguageModel session
/// each time Foundation Models evaluates them.
@available(macOS 27.0, iOS 27.0, visionOS 27.0, watchOS 27.0, *)
struct FoundationModelsDynamicInstructionsAdapter: FoundationModels.DynamicInstructions {
let session: LanguageModelSession

var body: some FoundationModels.DynamicInstructions {
let requestContext = session.resolvedRequestContext()
if let instructions = requestContext.instructions {
instructions.toFoundationModels()
}
requestContext.tools.toFoundationModels()
}
}

@available(macOS 27.0, iOS 27.0, visionOS 27.0, watchOS 27.0, *)
func makeFoundationModelsSession<Model: FoundationModels.LanguageModel>(
model: Model,
session: LanguageModelSession,
prompt: Prompt
) -> FoundationModels.LanguageModelSession {
if session.usesDynamicInstructions {
// Foundation Models evaluates the dynamic instructions itself,
// so the history leaves out the instructions entry.
let history = fmTranscriptDroppingDuplicatePrompt(
Transcript(
entries: session.transcript.filter { entry in
if case .instructions = entry { return false }
return true
}
),
prompt: prompt
).toFoundationModels(instructions: nil, toolDefinitions: [])
return FoundationModels.LanguageModelSession(
model: model,
dynamicInstructions: FoundationModelsDynamicInstructionsAdapter(session: session),
history: history
)
}

let requestContext = session.resolvedRequestContext()
return FoundationModels.LanguageModelSession(
model: model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import Testing
return false
}()

@available(macOS 27.0, iOS 27.0, visionOS 27.0, *)
private struct CompatibilityDynamicInstructions: DynamicInstructions {
let includeDetail: Bool

var body: some DynamicInstructions {
Instructions("You are a helpful assistant.")
if includeDetail {
Instructions("Include useful detail.")
}
}
}

@available(macOS 26.0, iOS 26.0, tvOS 26.0, visionOS 26.0, *)
@Test("AnyLanguageModel Drop-In Compatibility", .enabled(if: isSystemLanguageModelAvailable))
func anyLanguageModelCompatibility() async throws {
Expand All @@ -19,6 +31,14 @@ import Testing
instructions: Instructions("You are a helpful assistant.")
)

if #available(macOS 27.0, iOS 27.0, visionOS 27.0, *) {
_ = LanguageModelSession(
model: model,
dynamicInstructions: CompatibilityDynamicInstructions(includeDetail: true),
history: session.transcript
)
}

let options = GenerationOptions(temperature: 0.7)
let response = try await session.respond(options: options) {
Prompt("Say 'Hello'")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ import Testing
return false
}()

#if compiler(>=6.4)
#if os(macOS) || os(iOS) || os(visionOS)
@available(macOS 27.0, iOS 27.0, visionOS 27.0, *)
private struct CompatibilityDynamicInstructions: DynamicInstructions {
let includeDetail: Bool

var body: some DynamicInstructions {
Instructions("You are a helpful assistant.")
if includeDetail {
Instructions("Include useful detail.")
}
}
}
#endif
#endif

@available(macOS 26.0, iOS 26.0, tvOS 26.0, visionOS 26.0, *)
@Test(
"FoundationModels Drop-In Compatibility",
Expand All @@ -22,6 +38,18 @@ import Testing
instructions: Instructions("You are a helpful assistant.")
)

#if compiler(>=6.4)
#if os(macOS) || os(iOS) || os(visionOS)
if #available(macOS 27.0, iOS 27.0, visionOS 27.0, *) {
_ = LanguageModelSession(
model: model,
dynamicInstructions: CompatibilityDynamicInstructions(includeDetail: true),
history: session.transcript
)
}
#endif
#endif

let options = GenerationOptions(temperature: 0.7)
let response = try await session.respond(options: options) {
Prompt("Say 'Hello'")
Expand Down
Loading
Loading