Skip to content

Preserve dynamic profiles across tool continuations - #315

Open
qoli wants to merge 17 commits into
huggingface:mainfrom
qoli:codex/upstream-profile-tool-continuations
Open

qoli wants to merge 17 commits into
huggingface:mainfrom
qoli:codex/upstream-profile-tool-continuations

Conversation

@qoli

@qoli qoli commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • re-resolve the active Dynamic Profile before every provider-owned Tool continuation
  • execute Tool calls from the request context that produced them, even when the next profile changes tools or models
  • support profile-selected model handoff across OpenAI Chat, OpenAI Responses, Open Responses, Anthropic, Gemini, and Ollama flows
  • dispatch lifecycle, reasoning, Tool-call, Tool-output, and response callbacks against the producing snapshot
  • protect in-flight Tool entries from history mutation and capture transcript error policy per request
  • keep tool-round and repeated-call guards response-wide across model handoffs
  • merge canonical and current-round callback history by stable entry ID so completed Tool entries are not duplicated

Stack

This PR depends on #314, which in turn depends on #311 and #313. The branch includes the current #314 head and upstream main.

This does not overlap the native SystemLanguageModel Tool-delegate work in #297; it covers compatibility providers that own their continuation loop.

Validation

  • swift test --filter DynamicProfileTests — 15 tests passed
  • swift test --filter ProviderUsageTests — 35 tests passed, including response and streaming model-handoff limit regressions
  • swift test --filter AnthropicReasoningTests — 6 tests passed
  • swift test --filter SessionPropertyTests — 10 tests passed
  • strict Swift format lint on all changed source and test files
  • full Swift, iOS Simulator, and watchOS validation from the earlier branch revision

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

In-flight Tool history can be lost, wrapper models can recurse during initial dispatch, and one context option is unreachable.

3 open findings
What changed in this PR

Adds Foundation Models 27-compatible dynamic profiles and preserves profile-selected behavior across provider-owned Tool continuations.

Changes:

  • Adds session properties, request/context options, dynamic profiles, and lifecycle callbacks.
  • Re-resolves profiles between Tool rounds and supports model handoffs across compatibility providers.
  • Expands provider, compatibility, lifecycle, and history tests.
File Description
README.md Documents request options.
Sources/​AnyLanguageModel/​ContextOptions.swift Defines request-context options.
Sources/​AnyLanguageModel/​DynamicInstructions.swift Exposes instruction resolution internally.
Sources/​AnyLanguageModel/​DynamicProfile.swift Implements profiles, modifiers, and lifecycle handling.
Sources/​AnyLanguageModel/​GenerationOptions.swift Adds sampling alias and Tool-calling mode.
Sources/​AnyLanguageModel/​LanguageModelSession.swift Integrates profiles, properties, callbacks, and error policies.
Sources/​AnyLanguageModel/​ProfileToolExecution.swift Centralizes Tool execution and model handoffs.
Sources/​AnyLanguageModel/​SessionProperties.swift Implements session-scoped property and history storage.
Sources/​AnyLanguageModel/​Models/​AnthropicLanguageModel.swift Adds profile-aware Tool continuations.
Sources/​AnyLanguageModel/​Models/​GeminiLanguageModel.swift Adds profile-aware Tool continuations.
Sources/​AnyLanguageModel/​Models/​OllamaLanguageModel.swift Adds multi-round profile-aware Tool execution.
Sources/​AnyLanguageModel/​Models/​OpenAILanguageModel.swift Adds profile-aware Chat and Responses continuations.
Sources/​AnyLanguageModel/​Models/​OpenResponsesLanguageModel.swift Adds profile-aware Open Responses continuations.
Sources/​AnyLanguageModel/​Models/​SystemLanguageModel.swift Bridges Tool-calling mode to Foundation Models.
Sources/​AnyLanguageModelMacros/​Plugin.swift Registers the session-property macro.
Sources/​AnyLanguageModelMacros/​SessionPropertyEntryMacro.swift Generates session-property storage accessors.
Tests/​AnyLanguageModelTests/​AnthropicReasoningTests.swift Covers non-streaming Tool continuation reasoning.
Tests/​AnyLanguageModelTests/​APICompatibilityAnyLanguageModelTests.swift Checks compatibility profile APIs.
Tests/​AnyLanguageModelTests/​APICompatibilityFoundationModelsTests.swift Checks Foundation Models API parity.
Tests/​AnyLanguageModelTests/​DynamicProfileTests.swift Tests profile resolution and lifecycle behavior.
Tests/​AnyLanguageModelTests/​FoundationModelsGenerationOptionsTests.swift Tests Tool-mode bridging.
Tests/​AnyLanguageModelTests/​ProviderUsageTests.swift Tests provider continuations and model switching.
Tests/​AnyLanguageModelTests/​RequestOptionsTests.swift Tests request options and serialization.
Tests/​AnyLanguageModelTests/​SessionPropertyTests.swift Tests property storage, history, and observation.

🧠 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/ProfileToolExecution.swift
Comment thread Sources/AnyLanguageModel/SessionProperties.swift Outdated
Comment thread Sources/AnyLanguageModel/LanguageModelSession.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.

🟡 Changes recommended

Model handoffs can bypass tool-loop limits, while continuation history and typed property defaults have correctness defects.

3 open findings
3 resolved since last review

🧠 Review effort: Balanced

Comment thread Sources/AnyLanguageModel/ProfileToolExecution.swift
Comment thread Sources/AnyLanguageModelMacros/SessionPropertyEntryMacro.swift Outdated
Comment thread Sources/AnyLanguageModel/LanguageModelSession.swift Outdated
qoli added 4 commits October 15, 2026 09:03
…dynamic-profiles

# Conflicts:
#	Sources/AnyLanguageModel/LanguageModelSession.swift
#	Sources/AnyLanguageModel/SessionProperties.swift
#	Sources/AnyLanguageModelMacros/SessionPropertyEntryMacro.swift
#	Tests/AnyLanguageModelTests/APICompatibilityAnyLanguageModelTests.swift
#	Tests/AnyLanguageModelTests/APICompatibilityFoundationModelsTests.swift
#	Tests/AnyLanguageModelTests/SessionPropertyTests.swift

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

Unresolved history and handoff bugs can corrupt transcripts, break session isolation, and repeat Tool side effects.

5 open findings
3 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Filter instructions from tool execution history bindings

Sources/​AnyLanguageModel/​SessionProperties.swift:104

For an ordinary session with static instructions, withProfileToolExecution passes the full canonical request transcript into this binding. Unlike the unbound history getter, the binding does not filter instructions, so a Tool reading @SessionProperty(\.history) sees instruction entries despite HistoryView's instruction-free contract. Filter instructions when constructing the history binding.

🧠 Review effort: Balanced

Comment on lines +263 to +265
var context = resolveRequestContext(
additionalEntries: additionalEntries,
options: callSiteOptions
Comment on lines +532 to +536
var entryIDs = Set(requestContext.canonicalTranscript.map(\.id))
let additionalEntries = currentRoundEntries.filter { entryIDs.insert($0.id).inserted }
return (
requestContext.canonicalTranscript + additionalEntries,
Set(additionalEntries.map(\.id))
Comment on lines +1301 to +1304
try await profileDidProduceEntries(
Array(response.transcriptEntries) + [responseEntry],
requestContext: prepared.context,
currentRoundEntries: []
Comment on lines +29 to +31
guard !prefixEntries.isEmpty else { return nil }
if let selected = requestContext.model as? any ProfileRequestIdentifiedLanguageModel,
selected.profileRequestIdentity == currentModelIdentity
Comment on lines +50 to +58
return SessionPropertyBinding.history?.entries ?? historyGetter()
}
set {
_withSessionPropertyMutation(keyPath: \.history) {
let newValue = newValue.filter { entry in
if case .instructions = entry { return false }
return true
}
if let history = SessionPropertyBinding.history {
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.

2 participants