Fourth and sixth pieces of the Apple Intelligence tool-calling work
(see /Users/rune/.claude/plans/apple-intelligence-tool-calling-plan.md).
generateAppleOnDeviceToolResponse() is a new, dedicated dispatch path
— routed to instead of the generic generateAIResponseWithTools() when
currentProvider == .appleOnDevice, since LanguageModelSession's
persistent, framework-driven tool loop doesn't fit the stateless,
manually-looped chatWithToolMessages contract every other provider
uses. Builds the tool list + system prompt once per turn, calls
AppleFoundationProvider.respondWithTools() exactly once, and renders
the result through the exact same showSystemMessage/
flushToolCallSummary/updateToolCallMessage path every other provider's
tool loop already uses, so the "🔧 Calling: …" UI looks identical
regardless of which provider is actually running.
New appleSessionKey gives Apple On-Device a stable per-conversation
identity even before a chat is ever saved (currentConversationId is
nil until first ⌘S) — an ephemeral UUID regenerated on New Chat and
Clear Chat, which also now explicitly resets the cached tool session
on both of those actions so a stale session is never reused for what
the user sees as a different conversation.
Dormant until step 7 flips capabilities.tools for Apple On-Device —
modelSupportTools is still false today, so this new dispatch branch
never actually fires yet. 425 tests, stable across two consecutive
full runs, zero regressions in the existing dispatch/reset paths this
touched.