Skip to content

Add session properties and Foundation Models 27 HistoryView compatibility - #313

Open
qoli wants to merge 9 commits into
huggingface:mainfrom
qoli:codex/upstream-session-properties
Open

qoli wants to merge 9 commits into
huggingface:mainfrom
qoli:codex/upstream-session-properties

Conversation

@qoli

@qoli qoli commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add session-scoped properties to dynamic instructions and Tools, following the Foundation Models 27 contract while retaining OS 26 deployment support.

Review changes

  • Transcript.HistoryView is a value-type struct conforming to RandomAccessCollection, MutableCollection, RangeReplaceableCollection and ExpressibleByArrayLiteral, with a Hashable/Strideable Index and self-typed subsequences.
  • Preserve the 1.0 public LanguageModelSession.RequestContext and zero-argument resolvedRequestContext() APIs. New profile overloads/hooks remain Compatibility SPI in Add shared profile execution behind Compatibility SPI #315.
  • Tool.SessionProperty now belongs to this PR rather than Add shared profile execution behind Compatibility SPI #315.
  • Request Tools explicitly carry their session-property owner and instruction-free history snapshot. Detached execution and native wrappers re-establish the binding without relying on inherited TaskLocals.
  • History is read-only while dynamic instructions or a Tool is active. Custom values remain writable and isolated between sessions.
  • Defaults are evaluated outside the storage lock and rechecked before storage. SessionPropertyKey.Value has no extra Sendable constraint.

Fresh validation (2026-10-19)

  • Merged upstream main 371b142 with a normal history-preserving merge.
  • CI=1 GIT_CONFIG_GLOBAL=/dev/null swift test: 815 tests in 85 suites passed.
  • New regression uses ordinary import AnyLanguageModel, without SPI or @testable, to name RequestContext, call the zero-argument resolver, read its existing fields, and compile the Tool-scoped wrapper. It failed to compile before the fix and passes afterward.
  • The full run includes detached/static/dynamic Tool and native-wrapper session-property regressions, plus both package/SDK compatibility sources.
  • Strict Swift format lint on changed files and git diff --check passed.

The per-process Git configuration bypass avoids this machine's missing MLX mirror; no global configuration changed. CI mode skips live model-weight/Apple Intelligence generation.

Profile composition is separate in #314. Session construction/runtime and new provider-facing hooks remain Compatibility SPI in #315 pending the outstanding construction decision. No published history was rewritten.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Observation notifications, history filtering, and request snapshot consistency need correction.

3 open findings
What changed in this PR

Adds Foundation Models 27-compatible session properties and binds them during dynamic-instruction evaluation.

Changes:

  • Adds session-property storage, wrappers, and declaration macro.
  • Exposes mutable, instruction-free session history.
  • Adds behavioral and cross-framework compatibility tests.
File Description
SessionPropertyTests.swift Tests properties, history, and session isolation.
APICompatibilityFoundationModelsTests.swift Checks Foundation Models 27 compatibility.
APICompatibilityAnyLanguageModelTests.swift Checks matching AnyLanguageModel usage.
SessionPropertyEntryMacro.swift Generates property keys and accessors.
Plugin.swift Registers the new macro.
SessionProperties.swift Defines the session-property API and storage.
LanguageModelSession.swift Binds properties and connects history to sessions.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Sources/AnyLanguageModel/LanguageModelSession.swift Outdated
Comment thread Sources/AnyLanguageModel/LanguageModelSession.swift Outdated
Comment thread Sources/AnyLanguageModelMacros/SessionPropertyEntryMacro.swift Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

History accepts instruction entries, the key value constraint is incompatible with the mirrored API, and public documentation is stale.

0 open findings

3 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Constrain SessionPropertyKey.Value to Sendable

Sources/​AnyLanguageModel/​SessionProperties.swift:17

SessionPropertyKey.Value is missing Foundation Models 27's Sendable constraint. This allows a non-Sendable value to be stored in SessionPropertyValues, even though that container is exposed as @unchecked Sendable, and makes this protocol's public surface incompatible with the API it mirrors. Constrain the associated value type to Sendable.

Medium severity Filter instruction entries from writable HistoryView

Sources/​AnyLanguageModel/​SessionProperties.swift:54

The public HistoryView is an unrestricted [Transcript.Entry], so this setter currently accepts .instructions entries even though history is documented and exposed as instruction-free. Such entries remain in session.transcript but disappear from the next history read, and can duplicate preserved static instructions. Filter instruction entries here just as the dynamic-session initializer does.

Low severity Document session mutation during request context resolution

Sources/​AnyLanguageModel/​LanguageModelSession.swift:180

Binding writable session properties makes resolvedRequestContext() observably mutate the session when dynamic instructions update a property (as the new revision test demonstrates), so its existing API documentation—“Calling this method doesn't change the session”—is now false. Update that contract to describe the permitted property mutation.

🧠 Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

History binding can cross session boundaries, and history replacement can retain unwanted instructions.

0 open findings

Previously missed (2)

In code that hasn't changed since last review

Medium severity Filter instruction entries from incoming history

Sources/​AnyLanguageModel/​LanguageModelSession.swift:341

HistoryView is an unrestricted [Transcript.Entry], so replacing history with Array(otherSession.transcript) can persist that session's .instructions entries. The ordinary history getter hides them, but dynamic resolution snapshots the full transcript, exposing those entries through bound history and sending them alongside the newly resolved instructions. Filter .instructions out of incoming history before combining it with the preserved static-instructions prefix.

Medium severity Scope task-local history to the bound session

Sources/​AnyLanguageModel/​SessionProperties.swift:55

The task-local history is applied to every SessionPropertyValues instance, not just the bound session. While session A's dynamic instructions run, reading sessionB.properties.history returns A's snapshot, and replacing B's history incorrectly triggers the read-only precondition. Check SessionPropertyBinding.values === self before applying either the snapshot or the write restriction so unrelated sessions remain isolated.

🧠 Review effort: Balanced

@qoli

qoli commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both additional history findings from the latest review in 3ad28fb: task-local history is now scoped to its owning SessionPropertyValues instance, and incoming instruction entries are filtered before persistence. Added regressions for reading/writing a second session from a read-only instructions evaluation and replacing history from another session transcript. swift test --filter SessionPropertyTests passes 11 tests; strict format lint passes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Dependent defaults can deadlock, and history replacement can leave in-flight responses attached to the wrong conversation.

2 open findings

🧠 Review effort: Balanced

Comment thread Sources/AnyLanguageModel/LanguageModelSession.swift
Comment thread Sources/AnyLanguageModel/SessionProperties.swift Outdated
@mattt

mattt commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Thanks, @qoli, for all the work on this stack, and for keeping up with every round of review.

I'd like to get as much of this as we can into a 1.x release. The catch is that anything public in 1.x has to stay as it is until 2.0, so where Foundation Models 27 has an API, ours should have the same shape. A few changes would get us there:

  1. Could Transcript.HistoryView be a struct, like Apple's, conforming to the same collection protocols (RandomAccessCollection, MutableCollection, RangeReplaceableCollection, ExpressibleByArrayLiteral)? As a typealias for [Entry], code can rely on it being an array, and then it can't change later without breaking that code.
  2. The functions that only the providers call — resolvedRequestContext(including:options:), profileDidProduceEntries(_:requestContext:currentRoundEntries:), withProfileToolExecution(requestContext:currentRoundEntries:operation:), and the others like them — could go behind @_spi(Compatibility), the way Preserve attachments in tool outputs #312 does it.
  3. Could you split Add Foundation Models 27 dynamic profile composition APIs #314 and Add shared profile execution behind Compatibility SPI #315 into smaller PRs? For example, the profile API first, then the tool-loop changes, one provider at a time. Together they're about 5,600 lines across five providers, which is a lot to review at once. It'd also help us see which parts depend on how a session is constructed — Roadmap: 1.0 (Foundation Models 26) and 2.0 (Foundation Models 27) #210 expects that to change in 2.0, so those parts may need to wait for it.

One question: what happens when a tool that uses @SessionProperty runs with MLX, llama.cpp, or the system model? From reading the code, it looks like it might crash there.

@qoli qoli changed the title Add session properties to dynamic instructions Add session properties and Foundation Models 27 HistoryView compatibility Oct 17, 2026
@qoli

qoli commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

@mattt I’ve reshaped the stack around your review requests, preserving the existing branch histories (normal merges/commits, no force-push).

The provider integrations are separate sibling PRs:

PR Scope Dependencies
#344 OpenAI Chat Completions + Responses #315
#345 OpenResponses #315
#346 Anthropic #312, #315
#347 Gemini #312, #315
#348 Ollama #315
#349 Native history projection only #315

#349 does not add native Tool delegate/callback forwarding, so it stays separate from #297. Anthropic’s ordinary non-streaming, single-request Tool behavior is preserved; automatic continuation is profile-only. Upstream #343’s repeated-call behavior is preserved throughout.

The PR descriptions include dependency notes and isolated provider-review links. #317 remains draft: its retained public-construction example needs the construction contract settled before publication.

Integrated local verification on the dependency aggregate: 851 default tests; 1,017 with MLX/Llama/CoreML; 745 with AsyncHTTPClient. watchOS device build and watchOS Simulator build-for-testing also passed. Live weights / Apple Intelligence generation were not exercised. The current #313 workflow is awaiting maintainer approval, rather than reporting a code/test failure.

A final default build/test run also passed in a fresh SwiftPM scratch directory. Switching AsyncHTTPClient off in the reused .build had left importable AHC artifacts in the product search path (canImport(AsyncHTTPClient) in EventSource), causing inactive C-module resolution errors; isolation confirmed this was build-product contamination. No dependency source or lockfile changes were committed.

@mattt

mattt commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Thanks, @qoli. The split makes this much easier to follow, and HistoryView looks right now. A few things from going through it:

  1. 313: RequestContext and resolvedRequestContext() were already public in 1.0 — so models outside the package can read their inputs — and putting them behind SPI would break those models. Could you keep them public and put only the new overloads and hooks behind @_spi(Compatibility)? While you're there, Tool.SessionProperty could move into 313 from Add shared profile execution behind Compatibility SPI #315.
  2. 315: let's make init(profile:history:) public, with Apple's signature, so the profile types from Add Foundation Models 27 dynamic profile composition APIs #314 are usable in 1.2. Let callers set how many tool rounds a response takes #331 is about to land, so the round limit here will need to read options.maximumToolRounds.
  3. 314: Apple's historyTransform doesn't take a @Sendable closure. Could ours match, so the same code compiles against both?
  4. 344, 345, 346, 348: the replacement helpers drop entries whose ID is already in the transcript, and the tool output ID is the provider's call ID. If a server reuses call IDs across turns — call_0 every time, say — a request can silently lose that round's tool outputs, and that's for everyone, not only profile users. Could you move one shared helper into Add shared profile execution behind Compatibility SPI #315, keep transcript + inFlight when there's no profile, and add a test with the same call ID in two rounds that checks the second request? In Integrate dynamic profiles with Anthropic Tool loops #346, the in-flight messages are also built and encoded when there's no profile.
  5. 348: making non-streaming Ollama continue after tool calls is probably the right fix, but it changes things for everyone using Ollama. Could you move it into its own PR, so we can look at it alongside how Integrate dynamic profiles with Anthropic Tool loops #346 handles Anthropic?

@qoli

qoli commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, @mattt — items 1 and 3 are now addressed:

Both branches now include main 371b142 via normal merges. Fresh full SwiftPM runs passed: 815 tests / 85 suites for #313, and 822 / 86 for #314. The regressions were confirmed to fail to compile before their respective fixes. The remaining runtime/provider items are still being worked through separately.

@qoli

qoli commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on item 4 of #313 (comment): the common mapper is now in #315, and #344/#345/#346/#348 preserve the ordinary transcript + inFlight path. New regressions reuse the same call ID across rounds and turns and check the continuation requests. Anthropic's ordinary nonstreaming branch no longer builds/encodes unused in-flight messages.

All provider heads have been normally merged with the updated dependencies and main 303f8a1, verified, and pushed. The detailed shared fix and per-PR validation are in #315 and the provider descriptions. #317 remains draft; its integrated checks pass 930 tests / 92 suites by default and 798 / 83 with AsyncHTTPClient from an isolated scratch build.

Items 1 and 3 remain covered by the earlier public-API/Tool.SessionProperty and non-Sendable historyTransform fixes. Item 2's public profile initializer and item 5's separate ordinary Ollama PR are still outstanding, not silently treated as addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants