Add fm: Apple Foundation Models CLI#1
Open
planbnet wants to merge 4 commits into
Open
Conversation
2cf5734 to
e78f895
Compare
Adds the 'fm' command, registered via ios_system, exposing Apple Foundation Models (on-device 'system' and Private Cloud Compute 'pcc'): - respond / chat / token-count / schema / available - agent: interactive coding agent with tool calling (read/write/edit files, grep, glob, list_dir, run_shell, web_search, read_url). Defaults to the on-device model; pcc is opt-in and falls back to system. run_shell uses ios_system's interpreter (ios_popen) on device. Ctrl-C cancels generation. Sources live in a-Shell/fm_cmd/. Adds the private-cloud-compute entitlement (required for the pcc model). No signing/team or bundle-id changes.
e78f895 to
20d1827
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new fm CLI command to a-Shell that exposes Apple Foundation Models (system + Private Cloud Compute) and includes an interactive “agent” mode with tool-calling.
Changes:
- Introduces
a-Shell/fm_cmd/*implementingrespond,chat,token-count,available, andagentsubcommands (with streaming and Ctrl‑C cancellation). - Registers
fmwithios_system(replaceCommand("fm", "fm_main", true)), and adds the PCC entitlement. - Updates the Xcode project to include the new fm_cmd Swift sources.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| a-Shell/fm_cmd/Output.swift | Adds stream resolution helpers and ANSI utilities. |
| a-Shell/fm_cmd/Interrupt.swift | Implements SIGINT-based cancellation for async model work. |
| a-Shell/fm_cmd/Help.swift | Adds user-facing help text for all fm subcommands. |
| a-Shell/fm_cmd/FoundationModelsExecution.swift | Implements model execution for respond/chat/token-count/available with availability guards. |
| a-Shell/fm_cmd/FMCommand.swift | Adds dispatcher + ios_system entry point (fm_main). |
| a-Shell/fm_cmd/Commands.swift | Adds argument parsing for all subcommands. |
| a-Shell/fm_cmd/AgentTools.swift | Implements agent tool runner (files, grep/glob, shell, web). |
| a-Shell/fm_cmd/AgentFMTools.swift | Wraps runner tools into FoundationModels Tool types. |
| a-Shell/fm_cmd/AgentDisplay.swift | Adds terminal UI rendering + spinner/panels for agent mode. |
| a-Shell/fm_cmd/AgentCommand.swift | Implements agent REPL + streaming + fallback behaviors. |
| a-Shell/AppDelegate.swift | Registers fm command with ios_system. |
| a-Shell/a-Shell.entitlements | Adds Private Cloud Compute entitlement. |
| a-Shell.xcodeproj/project.pbxproj | Adds fm_cmd group + file refs + build phase entries. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+149
to
+155
| case "-m", "--model": | ||
| guard let val = parser.next() else { | ||
| fputs("Error: '--model' requires a value.\n", error) | ||
| return 64 | ||
| } | ||
| opts.model = val | ||
| case "-i", "--instructions": |
Comment on lines
+255
to
+261
| case "-m", "--model": | ||
| guard let val = parser.next() else { | ||
| fputs("Error: '--model' requires a value.\n", error) | ||
| return 64 | ||
| } | ||
| opts.model = val | ||
| default: |
Comment on lines
+59
to
+62
| let start = (args["start_line"] as? Int).map { max(1, $0) } ?? 1 | ||
| let end = (args["end_line"] as? Int).map { min(total, $0) } ?? total | ||
|
|
||
| let selected = lines[(start-1)..<end] |
Comment on lines
+46
to
+59
| func spin(_ message: String) { | ||
| q.async { [weak self] in | ||
| guard let self else { return } | ||
| self._clearSpin() | ||
| self.spinMsg = message | ||
| self.spinStart = Date() | ||
| self.spinIdx = 0 | ||
| let t = DispatchSource.makeTimerSource(queue: self.q) | ||
| t.schedule(deadline: .now(), repeating: .milliseconds(120)) | ||
| t.setEventHandler { [weak self] in self?._drawSpin() } | ||
| self.spinTimer = t | ||
| t.resume() | ||
| } | ||
| } |
Comment on lines
+975
to
+984
| FM0002000000000000000001 /* Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000001 /* Commands.swift */; }; | ||
| FM0002000000000000000002 /* FMCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000002 /* FMCommand.swift */; }; | ||
| FM0002000000000000000003 /* FoundationModelsExecution.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000003 /* FoundationModelsExecution.swift */; }; | ||
| FM0002000000000000000004 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000004 /* Help.swift */; }; | ||
| FM0002000000000000000005 /* Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000005 /* Output.swift */; }; | ||
| FM0002000000000000000007 /* AgentCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000007 /* AgentCommand.swift */; }; | ||
| FM0002000000000000000008 /* AgentDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000008 /* AgentDisplay.swift */; }; | ||
| FM0002000000000000000009 /* AgentFMTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000009 /* AgentFMTools.swift */; }; | ||
| FM000200000000000000000A /* AgentTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM000100000000000000000A /* AgentTools.swift */; }; | ||
| FM000200000000000000000C /* Interrupt.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM000100000000000000000C /* Interrupt.swift */; }; |
Comment on lines
+367
to
+368
| /// Build a `LanguageModelSession` using concrete model types to stay at iOS 26+ | ||
| /// for the system model path and iOS 27+ for PCC. |
- token-count: include --instructions in the counted text (was ignored). - chat / available: validate --model (reject unknown values, like respond). - read_file: guard start_line > end_line and start beyond EOF (no trap). - agent spinner: on non-TTY output, print one status line instead of spamming a new line every 120ms. - Regenerate the fm_cmd project object IDs as valid 24-char hex. - Fix a stale availability doc comment.
Comment on lines
+30
to
+32
| public static func isTerminal(_ file: UnsafeMutablePointer<FILE>) -> Bool { | ||
| isatty(fileno(file)) != 0 | ||
| } |
Comment on lines
+13
to
+15
| final class ErrorBox { | ||
| var error: Error? | ||
| } |
Comment on lines
+17
to
+41
| private var fmInterruptFlag: sig_atomic_t = 0 | ||
|
|
||
| private func fmInterruptHandler(_ sig: Int32) { | ||
| fmInterruptFlag = 1 | ||
| } | ||
|
|
||
| /// Runs `operation` while Ctrl-C cancels its Task. Returns `true` if it ran to | ||
| /// completion, `false` if it was interrupted by SIGINT. | ||
| @discardableResult | ||
| func runInterruptible(_ operation: @escaping () async -> Void) async -> Bool { | ||
| fmInterruptFlag = 0 | ||
| let previous = signal(SIGINT, fmInterruptHandler) | ||
| defer { signal(SIGINT, previous) } | ||
|
|
||
| let op = Task { await operation() } | ||
| let watcher = Task { | ||
| while !Task.isCancelled { | ||
| if fmInterruptFlag != 0 { op.cancel(); return } | ||
| try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s | ||
| } | ||
| } | ||
| await op.value | ||
| watcher.cancel() | ||
| return fmInterruptFlag == 0 | ||
| } |
Comment on lines
+180
to
+204
| private static func _execShell(command: String, timeout: Int) -> String { | ||
| let tmpPath = NSTemporaryDirectory() + "fm-shell-\(UUID().uuidString).out" | ||
| guard let fp = fopen(tmpPath, "w+") else { | ||
| return "Error: could not create a temporary file to capture output" | ||
| } | ||
|
|
||
| let savedOut = thread_stdout | ||
| let savedErr = thread_stderr | ||
| thread_stdout = fp | ||
| thread_stderr = fp | ||
|
|
||
| let pid = ios_fork() | ||
| _ = ios_system(command) | ||
| fflush(fp) | ||
| ios_waitpid(pid) | ||
| ios_releaseThreadId(pid) | ||
|
|
||
| thread_stdout = savedOut | ||
| thread_stderr = savedErr | ||
| fclose(fp) | ||
|
|
||
| let data = (try? Data(contentsOf: URL(fileURLWithPath: tmpPath))) ?? Data() | ||
| try? FileManager.default.removeItem(atPath: tmpPath) | ||
| return _truncateOutput(String(decoding: data, as: UTF8.self)) | ||
| } |
Pass images as Attachment(cgImage:) (decoded up front with ImageIO, preserving EXIF orientation) instead of Attachment(imageURL:). The URL initializer defers loading until the model processes the prompt; decoding up front fails fast on unreadable images and avoids a deferred-load stall. ImageIO/CoreGraphics only, no UIKit/AppKit.
The CGImage pre-decode didn't fix the image hang (a FoundationModels beta limitation, not the attachment path), so revert to the simpler, cleaner URL-based attachment.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Adds the
fmcommand, exposing Apple Foundation Models (on-device 'system' and Private Cloud Compute 'pcc').The command was introduced by Apple in macOS 27 Golden Gate and allows CLI access to the on-device Apple Foundation Model as well as Apple's new Private Cloud Compute model: https://wwdc.ai/2026/334
The subcommands
respond/chat/token-countandavailablework like they to in the new macOS 27 system tool.Besides that, I added
fm agentan interactive coding agent with tool calling (read/write/edit files, grep, glob, list_dir, run_shell, web_search, read_url). Defaults to the on-device model; pcc is opt-in and falls back to system. run_shell uses ios_system's interpreter (ios_popen) on device.fm agentwas inspired by https://github.com/mikeypdev/a-agent but does not need any API key and can run completely with the on device model (for very simpletasks though).Sources live in a-Shell/fm_cmd/. Adds the private-cloud-compute entitlement (required for the pcc model).
Caveats: