Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f2de319
Add Foundation Models 27 request options
qoli Oct 9, 2026
cd1d6fc
Add session properties to dynamic instructions
qoli Oct 11, 2026
9481380
Add session properties to dynamic instructions
qoli Oct 11, 2026
a03fd6b
Add Foundation Models 27 dynamic profiles
qoli Oct 11, 2026
da63029
Keep session property requests observable
qoli Oct 12, 2026
8b3a234
Keep session property requests observable
qoli Oct 12, 2026
d0cae54
Isolate dynamic profile request state
qoli Oct 13, 2026
d1c8413
Merge remote-tracking branch 'upstream/main' into codex/upstream-dyna…
qoli Oct 14, 2026
93e4d4d
Fix session property observation and typed defaults
qoli Oct 15, 2026
e99341e
Merge branch 'codex/upstream-session-properties' into codex/upstream-…
qoli Oct 15, 2026
cdeed51
Fix dynamic profile callback concurrency
qoli Oct 15, 2026
d211109
Allow sampling mode without tool calling policy
qoli Oct 16, 2026
3ad28fb
Keep session history bindings isolated and instruction free
qoli Oct 16, 2026
1fde50e
Merge branch 'codex/upstream-request-options' into codex/upstream-dyn…
qoli Oct 16, 2026
05f892a
Merge branch 'codex/upstream-session-properties' into codex/upstream-…
qoli Oct 16, 2026
9b76d74
Preserve profile history and request callback identity
qoli Oct 16, 2026
2c5c488
Invalidate stale history commits and evaluate property defaults outsi…
qoli Oct 17, 2026
905194e
Merge branch 'codex/upstream-session-properties' into codex/upstream-…
qoli Oct 17, 2026
d9e5f31
Preserve profile lifecycle identity and evolving callback history
qoli Oct 17, 2026
790ce15
Merge remote-tracking branch 'upstream/main' into codex/upstream-sess…
qoli Oct 17, 2026
0a3580b
Match HistoryView collection shape and bind session properties in con…
qoli Oct 17, 2026
4cf0f06
Reshape dynamic profiles as composition-only API on current session p…
qoli Oct 17, 2026
0dad115
Merge remote-tracking branch 'upstream/main' into codex/upstream-sess…
qoli Oct 19, 2026
25d51f1
Preserve public request context and expose Tool session properties
qoli Oct 19, 2026
8e10048
Merge session properties and upstream request options into profiles
qoli Oct 19, 2026
982cc07
Match the SDK history transform closure contract
qoli Oct 19, 2026
db6bc62
Preserve public Tool identity and bind at execution boundaries
qoli Oct 21, 2026
9b01280
Merge main while retaining offered Tool filtering and execution binding
qoli Oct 21, 2026
846508e
Merge branch 'codex/upstream-session-properties' into codex/upstream-…
qoli Oct 21, 2026
170aa66
Merge commit '7013d51f5f5249ab907ac8e2b5b5d0eb222c4153' into codex/up…
qoli Oct 22, 2026
aeae6e1
Merge commit '170aa66ef7024b72cbcefcf13a08621c9b466f5a' into codex/up…
qoli Oct 22, 2026
2495f61
Merge upstream request accounting and stream updates
qoli Oct 23, 2026
5359f3d
Merge updated session-properties dependency
qoli Oct 23, 2026
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
514 changes: 514 additions & 0 deletions Sources/AnyLanguageModel/DynamicProfile.swift

Large diffs are not rendered by default.

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)
}
}
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