Skip to content
Open
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
124 changes: 105 additions & 19 deletions Sources/AnyLanguageModel/LanguageModelSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ public final class LanguageModelSession: @unchecked Sendable {
private let dynamicInstructions: AnyDynamicInstructions?
private let dynamicInstructionsLock = NSLock()

/// Session-scoped values available to dynamic instructions.
public let properties: SessionPropertyValues

/// Whether the session resolves its instructions and tools before each request.
nonisolated var usesDynamicInstructions: Bool {
dynamicInstructions != nil
Expand Down Expand Up @@ -150,6 +153,16 @@ public final class LanguageModelSession: @unchecked Sendable {
/// The tools that the model can call in response to the request.
public let tools: [any Tool]

func toolsForExecution(in session: LanguageModelSession, offeredTools: [any Tool]? = nil) -> [any Tool] {
let history = Transcript.HistoryView(
transcript.filter {
if case .instructions = $0 { return false }
return true
}
)
return (offeredTools ?? tools).map { $0.bindingSessionProperties(session.properties, history: history) }
}

fileprivate init(transcript: Transcript, instructions: Instructions?, tools: [any Tool]) {
self.transcript = transcript
self.instructions = instructions
Expand All @@ -159,7 +172,9 @@ public final class LanguageModelSession: @unchecked Sendable {

/// Returns the transcript, instructions, and tools for the next model request.
///
/// Calling this method doesn't change the session.
/// Dynamic instructions may read and update custom session properties while
/// being resolved. Their history snapshot is read-only and instructions are
/// not committed to the canonical transcript.
///
/// - Note: This API is exclusive to AnyLanguageModel
/// and using it means your code is no longer drop-in compatible
Expand All @@ -168,16 +183,26 @@ public final class LanguageModelSession: @unchecked Sendable {
/// can read the inputs for each request.
nonisolated public func resolvedRequestContext() -> RequestContext {
guard let dynamicInstructions else {
return RequestContext(transcript: transcript, instructions: instructions, tools: tools)
let transcript = transcript
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 canonicalHistory = Transcript.HistoryView(transcript)
let resolved = dynamicInstructionsLock.withLock {
dynamicInstructions.resolveForRequest()
SessionPropertyBinding.$values.withValue(properties) {
SessionPropertyBinding.$history.withValue(canonicalHistory) {
dynamicInstructions.resolveForRequest()
}
}
}
var requestTranscript = transcript
var requestTranscript = Transcript(entries: Array(canonicalHistory))
if let instructions = resolved.instructions {
let instructionsEntry = Transcript.Entry.instructions(
Transcript.Instructions(
Expand All @@ -189,7 +214,11 @@ public final class LanguageModelSession: @unchecked Sendable {
)
requestTranscript = Transcript(entries: [instructionsEntry] + requestTranscript)
}
return RequestContext(transcript: requestTranscript, instructions: resolved.instructions, tools: resolved.tools)
return RequestContext(
transcript: requestTranscript,
instructions: resolved.instructions,
tools: resolved.tools
)
}

/// Creates a session with a model, tools,
Expand Down Expand Up @@ -324,7 +353,52 @@ public final class LanguageModelSession: @unchecked Sendable {
}
}

self.state = .init(.init(finalTranscript))
let state = Locked(State(finalTranscript))
self.state = state
let setHistory: @Sendable (Transcript.HistoryView) -> Void = { history in
let history = history.filter { entry in
if case .instructions = entry { return false }
return true
}
state.withLock { locked in
let instructions = locked.transcript.prefix {
if case .instructions = $0 { return true }
return false
}
locked.transcript = Transcript(entries: Array(instructions) + history)
Comment thread
qoli marked this conversation as resolved.
}
}
let observedHistorySetter = Locked<(@Sendable (Transcript.HistoryView) -> Void)?>(nil)
self.properties = SessionPropertyValues(
historyGetter: {
state.withLock { locked in
Transcript.HistoryView(
locked.transcript.filter { entry in
if case .instructions = entry { return false }
return true
}
)
}
},
historySetter: { history in
if let setter = observedHistorySetter.withLock({ $0 }) {
setter(history)
} else {
setHistory(history)
}
}
)
observedHistorySetter.withLock { setter in
setter = { [weak self] history in
guard let self else {
setHistory(history)
return
}
self.withMutation(keyPath: \.transcript) {
setHistory(history)
}
}
}
}

private static func instructions(from transcript: Transcript) -> Instructions? {
Expand Down Expand Up @@ -373,10 +447,18 @@ public final class LanguageModelSession: @unchecked Sendable {
}
}

nonisolated private func withTranscriptMutation<Result>(
_ mutation: () throws -> Result
) rethrows -> Result {
try properties._withSessionPropertyMutation(keyPath: \.history) {
try withMutation(keyPath: \.transcript, mutation)
}
}

// The prompt is the only entry committed before a request succeeds. Checkpoints and
// the final response commit atomically at completion, so rollback owns only this ID.
nonisolated private func removePrompt(id: String) {
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { state in
state.transcript = Transcript(entries: state.transcript.filter { $0.id != id })
}
Expand Down Expand Up @@ -485,17 +567,19 @@ public final class LanguageModelSession: @unchecked Sendable {
providerMetadata: lastSnapshot.providerMetadata
)
)
session.withMutation(keyPath: \.transcript) {
session.state.withLock {
$0.transcript.append(contentsOf: lastSnapshot.transcriptEntries)
$0.transcript.append(responseEntry)
session.withTranscriptMutation {
session.state.withLock { state in
guard state.transcript.contains(where: { $0.id == promptEntry.id }) else { return }
state.transcript.append(contentsOf: lastSnapshot.transcriptEntries)
state.transcript.append(responseEntry)
}
}
session.endResponding()
continuation.finish()
} catch {
session.withMutation(keyPath: \.transcript) {
session.withTranscriptMutation {
session.state.withLock { state in
guard state.transcript.contains(where: { $0.id == promptEntry.id }) else { return }
if state.transcriptErrorHandlingPolicy == .preserveTranscript {
state.transcript.append(contentsOf: lastSnapshot?.transcriptEntries ?? [])
} else if state.transcriptErrorHandlingPolicy == .revertTranscript {
Expand Down Expand Up @@ -751,7 +835,7 @@ public final class LanguageModelSession: @unchecked Sendable {
responseFormat: responseFormat
)
)
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { $0.transcript.append(promptEntry) }
}

Expand All @@ -777,8 +861,9 @@ public final class LanguageModelSession: @unchecked Sendable {
)

// Add tool entries and response to transcript
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { lockedState in
guard lockedState.transcript.contains(where: { $0.id == promptID }) else { return }
lockedState.transcript.append(contentsOf: response.transcriptEntries)
lockedState.transcript.append(responseEntry)
}
Expand Down Expand Up @@ -810,7 +895,7 @@ public final class LanguageModelSession: @unchecked Sendable {
responseFormat: type == String.self ? nil : .init(type: type)
)
)
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { $0.transcript.append(promptEntry) }
}

Expand Down Expand Up @@ -970,7 +1055,7 @@ extension LanguageModelSession {
responseFormat: .init(schema: schema)
)
)
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { $0.transcript.append(promptEntry) }
}

Expand Down Expand Up @@ -1173,7 +1258,7 @@ extension LanguageModelSession {
responseFormat: type == String.self ? nil : .init(type: type)
)
)
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { $0.transcript.append(promptEntry) }
}

Expand Down Expand Up @@ -1210,8 +1295,9 @@ extension LanguageModelSession {
)

// Add tool entries and response to transcript
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { lockedState in
guard lockedState.transcript.contains(where: { $0.id == promptID }) else { return }
lockedState.transcript.append(contentsOf: response.transcriptEntries)
lockedState.transcript.append(responseEntry)
}
Expand Down Expand Up @@ -1305,7 +1391,7 @@ extension LanguageModelSession {
responseFormat: type == String.self ? nil : .init(type: type)
)
)
withMutation(keyPath: \.transcript) {
withTranscriptMutation {
state.withLock { $0.transcript.append(promptEntry) }
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ public struct AnthropicLanguageModel: LanguageModel {
if !toolUses.isEmpty {
let resolution = try await resolveToolUses(
toolUses,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
)
switch resolution {
Expand Down Expand Up @@ -742,7 +742,7 @@ public struct AnthropicLanguageModel: LanguageModel {
try toolRounds.record(completed: completedEntries)
switch try await resolveToolUses(
toolUses,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
) {
case .stop(let calls):
Expand Down
4 changes: 2 additions & 2 deletions Sources/AnyLanguageModel/Models/GeminiLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ public struct GeminiLanguageModel: LanguageModel {
try toolRounds.record(completed: entries)
let resolution = try await resolveFunctionCalls(
functionCalls,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
)
switch resolution {
Expand Down Expand Up @@ -501,7 +501,7 @@ public struct GeminiLanguageModel: LanguageModel {
try toolRounds.record(completed: state.entries)
switch try await resolveFunctionCalls(
functionCalls,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
) {
case .stop(let calls):
Expand Down
4 changes: 2 additions & 2 deletions Sources/AnyLanguageModel/Models/LlamaLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ import Foundation

let resolution = try await resolveToolCalls(
parsedCalls,
tools: tools,
tools: requestContext.toolsForExecution(in: session, offeredTools: tools),
session: session
)
switch resolution {
Expand Down Expand Up @@ -1198,7 +1198,7 @@ import Foundation

let resolution = try await self.resolveToolCalls(
parsedCalls,
tools: tools,
tools: requestContext.toolsForExecution(in: session, offeredTools: tools),
session: session
)
switch resolution {
Expand Down
4 changes: 2 additions & 2 deletions Sources/AnyLanguageModel/Models/MLXLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,7 @@ import Foundation

let resolution = try await resolveToolCalls(
collectedToolCalls,
tools: tools,
tools: requestContext.toolsForExecution(in: session, offeredTools: tools),
session: session
)
switch resolution {
Expand Down Expand Up @@ -1444,7 +1444,7 @@ import Foundation

let resolution = try await resolveToolCalls(
collectedToolCalls,
tools: tools,
tools: requestContext.toolsForExecution(in: session, offeredTools: tools),
session: session
)
switch resolution {
Expand Down
4 changes: 2 additions & 2 deletions Sources/AnyLanguageModel/Models/OllamaLanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public struct OllamaLanguageModel: LanguageModel {
if let toolCalls = chatResponse.message.toolCalls, !toolCalls.isEmpty {
let resolution = try await resolveToolCalls(
toolCalls,
tools: tools,
tools: requestContext.toolsForExecution(in: session, offeredTools: tools),
session: session
)
switch resolution {
Expand Down Expand Up @@ -338,7 +338,7 @@ public struct OllamaLanguageModel: LanguageModel {
}
switch try await resolveToolCalls(
toolCalls,
tools: offeredTools,
tools: requestContext.toolsForExecution(in: session, offeredTools: offeredTools),
session: session
) {
case .stop(let calls):
Expand Down
6 changes: 3 additions & 3 deletions Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ public struct OpenAILanguageModel: LanguageModel {
try toolRounds.record(completed: entries)
let resolution = try await resolveToolCalls(
toolCalls,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
)
switch resolution {
Expand Down Expand Up @@ -707,7 +707,7 @@ public struct OpenAILanguageModel: LanguageModel {
try toolRounds.record(completed: entries)
let resolution = try await resolveToolCalls(
toolCalls,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
)
switch resolution {
Expand Down Expand Up @@ -944,7 +944,7 @@ public struct OpenAILanguageModel: LanguageModel {
try toolRounds.record(completed: state.entries)
switch try await resolveToolCalls(
toolCalls,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
) {
case .stop(let calls):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ public struct OpenResponsesLanguageModel: LanguageModel {
try toolRounds.record(completed: state.entries)
switch try await resolveToolCalls(
toolCalls,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
) {
case .stop(let calls):
Expand Down Expand Up @@ -685,7 +685,7 @@ public struct OpenResponsesLanguageModel: LanguageModel {
try toolRounds.record(completed: entries)
let resolution = try await resolveToolCalls(
toolCalls,
tools: requestContext.tools,
tools: requestContext.toolsForExecution(in: session),
session: session
)
switch resolution {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
// Attach the feedback to the session's conversation, including its latest response.
let fmSession = FoundationModels.LanguageModelSession(
model: pccModel,
tools: requestContext.tools.toFoundationModels(),
tools: requestContext.toolsForExecution(in: session).toFoundationModels(),
transcript: requestContext.transcript.toFoundationModels(
instructions: requestContext.instructions,
toolDefinitions: requestContext.tools
Expand Down
Loading
Loading