You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add session properties and Foundation Models 27 HistoryView compatibility - #313
Session-scoped properties for dynamic instructions and Tools, following Foundation Models 27 while retaining OS 26 deployment support.
HistoryView is a value-type struct with the SDK collection contract, Hashable/Strideable Index and self-typed subsequences.
Preserve the original 1.0 public RequestContext and zero-argument resolvedRequestContext(). Its Tools retain original concrete types/instances; ordinary-import regressions check type casts and reference identity.
Tool.SessionProperty belongs to this PR. Binding happens at execution/native boundaries, including detached execution, without inherited TaskLocals.
Active Tool/dynamic-instructions history is read-only; custom values remain writable and session-isolated. Defaults are evaluated outside the storage lock and rechecked before storage; no extra Sendable constraint on SessionPropertyKey.Value.
Upstream 1.1 synchronization
Normal history-preserving merges synchronize upstream main 7d43219, including #357 per-request usage accounting, #341 Ollama reasoning, #358 host-scoped Chat Completions reasoning, #300 response-start timeouts, #362 cancellation collection, #364 partial structured snapshots and #366/#367 bounds/reasoning/system-Tool fixes. Earlier #336 final-request usage, #340 HTTPTransport, #297/#325/#331/#343/#352/#354/#356 remain intact. No force-push, new checkout, replacement PR or extra fallback.
The original public RequestContext/raw-Tool identity and explicit detached/native session-property bindings remain unchanged. Upstream's request-usage TaskLocal is accounting only, not a substitute for explicit Tool property binding.
Fresh local verification (2026-10-23)
Head 2495f61. CI=1 GIT_CONFIG_GLOBAL=/dev/null swift test: 906 tests / 97 suites passed. The separate upstream macro test target also passed 2 tests / 1 suite. Strict formatting/diff checks passed. These are compile/offline tests, not live API, model-weight or Apple Intelligence inference acceptance.
Profile composition remains separate in #314. The new profile/runtime fields, overloads, hooks and constructor remain Compatibility SPI in #315, pending the existing construction decision.
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.
Filter instruction entries from writable HistoryView
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.
Document session mutation during request context resolution
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.
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.
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.
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.
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:
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.
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.
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
changed the title
Add session properties to dynamic instructions
Add session properties and Foundation Models 27 HistoryView compatibility
Oct 17, 2026
Add session properties and Foundation Models 27 HistoryView compatibility #313: HistoryView is a struct with the SDK’s collection conformances, custom Index and self-typed slices. RequestContext/resolution is Compatibility SPI. I also found and fixed the Tool binding gap: request Tools now carry their owning session properties/history and re-establish that binding at call time. Native FoundationModels.Tool wrappers are tested from detached tasks for two sessions, without inherited TaskLocal values. MLX/llama.cpp code paths compile and relevant tests pass; I have not run live model-weight generation.
#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.
Thanks, @qoli. The split makes this much easier to follow, and HistoryView looks right now. A few things from going through it:
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.
314: Apple's historyTransform doesn't take a @Sendable closure. Could ours match, so the same code compiles against both?
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.
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?
Add Foundation Models 27 dynamic profile composition APIs #314 at 982cc07 removes @sendable from historyTransform and its stored closure types. Equivalent non-Sendable function values compile against both AnyLanguageModel and the installed FoundationModels 27 SDK; an additional runtime test exercises a captured non-Sendable reference.
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.
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.
Thanks, @qoli. The shared continuation helper and the round-scoped IDs look good, and they fix the lost tool outputs. A few things left:
313:resolvedRequestContext() now returns the tools wrapped in SessionPropertyTool, so code outside the package that checks tool as? MyTool would get a different answer than in 1.0. Could the binding happen somewhere else, so it returns the tools as given?
315:init(profile:history:) is still SPI. Let's make it public — the signature already matches Apple's. What should happen when a profile doesn't set a model? Right now, the first request throws.
315: now that ToolRoundLimit is a class, every var toolRounds gives a warning. Could those be let? MLX and Llama also don't use session.toolRoundLimit(for:options:), so they don't share the session's round budget. Could they?
344-347: these need Add shared profile execution behind Compatibility SPI #315's latest head merged in. Also, resolveToolCalls makes a temporary ToolCalls entry, so profile hooks and tools see a ToolCalls entry ID that never shows up in the transcript. Could they get the committed entry instead?
348: could the non-streaming Ollama continuation go in its own PR, as I asked before?
349: a question: the Foundation Models adapter calls resolvedRequestContext() again inside its body. If Foundation Models evaluates that outside the request, does the profile get resolved twice?
Thanks, @mattt. I reproduced and addressed items 1, 3, 4 and 6 with normal dependency merges:
Add session properties and Foundation Models 27 HistoryView compatibility #3139b01280: public RequestContext.tools returns the original Tool values/instances. Session-property wrappers are applied only at execution/native boundaries. An ordinary-import regression first reproduced concrete-type and reference-identity failures; the full suite now passes 841 / 88.
Add shared profile execution behind Compatibility SPI #31557094db: ToolRoundLimit references are let; MLX/Llama use the shared session limit and accumulated recorder. Default verification passed 881 / 91, and a separate MLX,Llama,CoreML scratch build passed 1047 / 101 in CI mode.
Project profile history into native Foundation Models sessions #34930dd238: yes, the old factory could resolve a profile twice while constructing history and evaluating native instructions. Regressions reproduced that and detached evaluation without TaskLocal inheritance. The initial adapter evaluation now consumes the context already used for history; subsequent evaluations resolve afresh. Ordinary dynamic-instructions timing is unchanged. Full verification: 885 / 92, without Apple Intelligence inference.
Constructor visibility/default-model policy and the separate ordinary Ollama continuation PR are still pending scope confirmation; the constructor remains SPI, a missing model still reports the existing explicit error, and no new default model or PR was introduced.
The draft aggregate #3176bde2c0 contains every latest dependency head. Fresh integrated runs passed 933/92 (default), 801/83 (AsyncHTTPClient) and 1099/102 (MLX/Llama/CoreML), using separate trait scratch paths in the same checkout. These are compile/offline checks, not live API/model-weight acceptance. The original independent README delta remains 64 lines and the PR stays draft.
Thanks for items 1, 3, 4, and 6, @qoli. For the other two:
315: Yes, let's make init(profile:history:) public, with Apple's signature and no default model. Foundation Models falls back to the system model, but everywhere else we require a model, so let's keep that here and say so in the doc comment. Could the check happen before onActivate and onPrompt run, with an error that says to add .model(_:)?
348: Could the non-streaming Ollama continuation go in its own PR?
Thanks for clarifying the no-default-model and early missing-model validation requirement.
Public construction (including that preflight validation) and the standalone Ollama non-streaming PR remain outstanding in this update: init(profile:history:) stays Compatibility SPI, no default model is selected, #348 retains its existing behavior, and #317 stays draft. I have not marked those two scope items done.
Separately, all existing implementation branches and the draft aggregate are now synchronized with main 7d43219 through normal history-preserving merges:
Fresh local CI-mode verification of aggregate b946bec: default 1009 tests/101 suites, AsyncHTTPClient 851/91, MLX+Llama+CoreML 1175/111; the new upstream macro-test target separately passed 2 tests/1 suite in each configuration. #315 also passed default 949/100 and local traits 1115/110. These are compile/offline checks, not live model/API acceptance.
The aggregate contains all ten implementation heads, provider files match their owning PRs, and the original independent profile README delta remains 64 added lines. PR descriptions include each latest head's verification.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope
Session-scoped properties for dynamic instructions and Tools, following Foundation Models 27 while retaining OS 26 deployment support.
Upstream 1.1 synchronization
Normal history-preserving merges synchronize upstream main 7d43219, including #357 per-request usage accounting, #341 Ollama reasoning, #358 host-scoped Chat Completions reasoning, #300 response-start timeouts, #362 cancellation collection, #364 partial structured snapshots and #366/#367 bounds/reasoning/system-Tool fixes. Earlier #336 final-request usage, #340 HTTPTransport, #297/#325/#331/#343/#352/#354/#356 remain intact. No force-push, new checkout, replacement PR or extra fallback.
The original public RequestContext/raw-Tool identity and explicit detached/native session-property bindings remain unchanged. Upstream's request-usage TaskLocal is accounting only, not a substitute for explicit Tool property binding.
Fresh local verification (2026-10-23)
Head 2495f61.
CI=1 GIT_CONFIG_GLOBAL=/dev/null swift test: 906 tests / 97 suites passed. The separate upstream macro test target also passed 2 tests / 1 suite. Strict formatting/diff checks passed. These are compile/offline tests, not live API, model-weight or Apple Intelligence inference acceptance.Profile composition remains separate in #314. The new profile/runtime fields, overloads, hooks and constructor remain Compatibility SPI in #315, pending the existing construction decision.