tclai-apple
A Tcl extension that bridges Apple's Foundation Models framework (the on-device LLM powering Apple Intelligence, macOS 26+) to Tcl. Built with practcl using its Swift language support.
Requirements
- macOS 26 or later, with Apple Intelligence enabled
- Tcl 8.6 or later
- practcl 0.18+ (with Swift toolset support)
- Xcode command line tools (for
swiftc)
Building
cd deps/sobyk-src/tclai-apple
tclsh make.tcl library
This produces libapplefm1.0.dylib and pkgIndex.tcl in the current directory. The build uses practcl to compile the Swift source via swiftc -emit-library, linking against the system FoundationModels and Foundation frameworks.
Installation
tclsh make.tcl install /usr/local/lib/tcl
Or manually copy libapplefm1.0.dylib and pkgIndex.tcl to a directory on your auto_path.
Usage
Loading
load ./libapplefm1.0.dylib Applefm
Or via package require (if installed on auto_path):
package require applefm
Checking availability
applefm::availability
# → "available"
applefm::available
# → 1 (or 0 if unavailable)
availability returns one of:
- available — the model is ready
- unavailable apple_intelligence_not_enabled — Apple Intelligence is off
- unavailable model_not_ready — model not yet downloaded
- unavailable unknown — other
Synchronous completion
set reply [applefm::respond "Say hello in one word"]
puts $reply
applefm::respond blocks the Tcl thread until the model responds. Optional -instructions sets a system prompt:
set reply [applefm::respond "Describe this scene" -instructions "You are a dungeon master"]
Asynchronous completion (promise/future)
For non-blocking use — especially from coroutines — use the promise pattern:
# Start the request, get a token immediately
set token [applefm::ask "Say hello in one word"]
# ... do other work ...
# Wait for the result (yields to event loop, coroutine-safe)
set result [applefm::wait $token]
puts $result
The low-level commands:
applefm::ask $prompt ?-instructions $sys?— starts the request, returns a token string (e.g.applefm0)applefm::ready $token— returns1if the result is available,0if still pendingapplefm::get $token— retrieves the result and deletes the token. Error if not ready.applefm::wait $token— high-level helper: polls viaafter/vwaituntil ready, then callsget. Coroutine-safe.
Multi-turn conversations (sessions)
The one-shot commands (respond, ask) create a fresh LanguageModelSession each call, so the model sees no prior context. For multi-turn conversations, create a session — the session holds a LanguageModelSession that maintains a transcript across calls.
# Create a session with optional system instructions
set sid [applefm::session create -instructions "You are a concise assistant"]
# Synchronous multi-turn
puts [applefm::session respond $sid "My name is Bob."]
puts [applefm::session respond $sid "What is my name?"]
# → "Bob."
# Asynchronous multi-turn
set tok [applefm::session ask $sid "What did I tell you?"]
puts [applefm::session wait $tok]
# → "You told me your name is Bob."
# Clean up when done
applefm::session destroy $sid
Session commands:
- applefm::session create ?-instructions $sys? — creates a session, returns a session id (e.g. session0)
- applefm::session respond $sessionId $prompt — synchronous completion with conversation history
- applefm::session ask $sessionId $prompt — async: returns a token, starts a Task
- applefm::session wait $token — polls until ready (coroutine-safe), returns result
- applefm::session set-transcript $sessionId $entries — inject arbitrary conversation history
- applefm::session get-transcript $sessionId — read back the full transcript
- applefm::session destroy $sessionId — destroys the session and frees its resources
Transcript injection
For harnesses that manage context externally (compaction, summarization, background material), set-transcript replaces the session's entire conversation history. The format is a flat list of {role text} pairs:
applefm::session set-transcript $sid {
system "You are a concise assistant."
user "My name is Alice and I like turtles."
assistant "Nice to meet you, Alice!"
}
# The model now sees the injected history
puts [applefm::session respond $sid "What do I like?"]
# → "You like turtles."
# Read back the full transcript (including new turns)
foreach {role text} [applefm::session get-transcript $sid] {
puts "$role: $text"
}
Roles: system, user, assistant, reasoning, toolcall, tooloutput. UUIDs are auto-generated.
Coroutine example
coroutine chat apply {{} {
set sid [applefm::session create -instructions "You are a helpful assistant"]
set tok [applefm::session ask $sid "What is the capital of France?"]
set answer [applefm::session wait $tok]
puts "Answer: $answer"
applefm::session destroy $sid
}}
vwait forever
Architecture
The promise pattern
The async path avoids all cross-thread Tcl API calls. The flow:
applefm::askcreates a token (integer) via a thread-safeResultStore, starts a SwiftTask, and returns the token immediately.- The Swift
TaskrunsLanguageModelSession.respond(to:)on Swift's cooperative thread pool. When done, it writes the result to theResultStore(protected byNSLock). applefm::waitpollsapplefm::readyin a loop usingafter 10+vwait, yielding back to the Tcl event loop. This makes it safe to call from coroutines.applefm::getretrieves and deletes the result from the store.
No Tcl_Eval, Tcl_AsyncMark, or event queue manipulation happens from the Swift thread. The Swift side is pure computation; the Tcl side handles all event loop integration.
Session store
For multi-turn conversations, LanguageModelSession instances are held in a SessionStore — a thread-safe dictionary keyed by integer id, mirroring the ResultStore pattern. The Tcl side references sessions by string tokens (session0, session1, …). Reusing the same session across calls gives the model the full conversation transcript, enabling context-aware follow-up questions.
Practcl integration
The extension is built entirely with practcl's build system:
generic/applefm.swift— the Swift bridge. Uses@_cdecl("Applefm_Init")for the package entry point. Imports Tcl's C API via a module map.generic/module.modulemap+generic/tcl_module_header.h— wrapstcl.hso Swift canimport CTcland call Tcl's C API directly.cmodules/applefm/applefm.tcl— Tcl bootstrap code, injected into the Swift dylib via practcl'smy code tcl {...}mechanism. The Tcl code is embedded as a string literal in a generated Swift file (applefm_bootstrap.swift) and evaluated byApplefm_InitviaTcl_Evalat load time.cmodules/applefm/module.ini— practcl module declaration: declares the Swift source, the Tcl bootstrap, the module map directory, and the Apple frameworks to link.make.tcl+library.ini— practcl build driver. SourcestclConfig.shfor compiler settings, creates a::practcl::library, and builds viaswiftc -emit-library.
Swift toolset for practcl
This project drove the addition of Swift language support to practcl:
product.swiftfile— product class for.swiftsources (analogous toproduct.csource)toolset/swift.tcl— toolset mixin that knowsswiftc -emit-library, module maps, framework linking, and generates the Tcl bootstrap Swift filegenerate-swift-bootstrap— mirrors the Cgenerate-tcl-premechanism: collectsmy code tcl {...}from sub-products and emits them as Swift string literals
Commands reference
| Command | Args | Description |
|---|---|---|
applefm::availability |
Returns availability status string | |
applefm::available |
Returns 1 if available, 0 otherwise | |
applefm::respond |
prompt ?-instructions sys? |
Synchronous completion (blocks) |
applefm::ask |
prompt ?-instructions sys? |
Async: returns token, starts Task |
applefm::ready |
token |
Returns 1 if result is available |
applefm::get |
token |
Retrieves result, deletes token |
applefm::wait |
token |
High-level: polls until ready, returns result |
applefm::session create |
?-instructions sys? |
Creates a session, returns session id |
applefm::session respond |
sessionId prompt |
Sync completion with conversation history |
applefm::session ask |
sessionId prompt |
Async: returns token, starts Task |
applefm::session wait |
token |
Polls until ready (coroutine-safe) |
applefm::session set-transcript |
sessionId entries |
Inject arbitrary conversation history |
applefm::session get-transcript |
sessionId |
Read back full transcript as {role text} pairs |
applefm::session destroy |
sessionId |
Destroys the session |
applefm::version |
Returns package version (1.0) |
License
The Tcl Community License. (BSD basically.)