This document collects the lower-level architecture, packaging, and boundary notes for SwiftAgent. The README stays focused on app adoption.
Vendor/Python.xcframework: BeeWare Python 3.14 for iOS.Vendor/SwiftAgentISH.xcframework: a local iSH ARM64 static XCFramework for the embedded Linux-like shell backend.Vendor/ish-arm64: vendored GPLv3 iSH ARM64 source plus SwiftAgent embedding patches.Vendor/hermes-agent.lock: the reviewed upstream Hermes release pin.Payloads/Hermes/PythonApp: checked-in Python dependency layers plus a generatedhermes/source directory fetched from the lock file.Sources/CHermesPython: a C bridge aroundPyConfig, Python evaluation, and Hermes callback plumbing.Sources/SwiftAgentCore: the lightweight protocol/path layer and test doubles.Sources/SwiftAgent: the batteries-included iOS POC target that re-exportsSwiftAgentCoreand includes Hermes, iSH shell, and iOS shell support.Packages/SwiftAgentMLX: an optional local-model add-on package that carries the MLX/Hugging Face dependencies andSwiftAgentMLXModelProvider.Packages/SwiftAgentFoundationModels: an optional iOS 26+ add-on package that carriesSwiftAgentFoundationModelsProviderfor Apple Foundation Models.Examples/HermesAgentSample: an iOS app that consumes the canonical SwiftAgent Hermes payload through the build script.Scripts/update-hermes.sh: fetches the pinned upstream Hermes release and stages it into ignoredPayloads/Hermes/PythonApp/hermes.Scripts/build-native-wheels.sh: a reproducible recipe for rebuilding the Rust-backed iOS wheels used by OpenAI/Pydantic.
SwiftAgent is distributed under GPLv3. This matches the GPLv3 requirements of the embedded iSH dependency. Hermes Agent is MIT-licensed; the SwiftAgent build fetches and packages the pinned Hermes source, but the upstream Hermes license remains MIT.
The public layering is:
SwiftAgentShellEnvironment: runs shell commands for an agent. Current implementations areSwiftAgentISHShellEnvironmentand the olderSwiftAgentIOSShellEnvironment.SwiftAgentModelProvider: completes OpenAI-style model requests. Current implementations are the optionalSwiftAgentMLXModelProviderandSwiftAgentFoundationModelsProvideradd-ons plusSwiftAgentMockModelProvider.HermesAgentBackend: owns the execution boundary for a Hermes agent.HermesInProcessBackendruns in the host app.HermesAgentRuntime: the Hermes-specific adapter that embeds CPython, loads Hermes, and routes Hermes callbacks into the configured SwiftAgent shell/model providers.SwiftAgentMockShellEnvironmentandSwiftAgentMockModelProvider: test doubles for exercising the Hermes bridge without a full app or real model.
This gives us a clean path for future agent implementations: they should target the SwiftAgent protocols, while Hermes-specific Python and bootstrap code stays behind HermesAgentRuntime.
SwiftAgent does not expose package-level singleton instances. Each HermesAgent gets its own runtime facade and provider objects by default, and callers can inject custom shell/model implementations for tests or app-specific behavior. The embedded CPython interpreter is still process-global, so native Python calls are serialized internally; independent agent objects are supported, but true simultaneous Hermes execution needs deeper interpreter/session isolation.
SwiftAgent currently runs full Hermes in process. The prior worker-isolation experiment was removed because it did not reliably run full Hermes on device and doubled the sample app payload.
Use Swift Package Manager for the public integration. It is the best fit for SwiftAgent because it can deliver Swift source, binary XCFrameworks, resources, tests, scripts, and templates in one dependency.
SwiftAgent already uses XCFrameworks internally for the parts that need them:
Python.xcframeworkfor BeeWare Python.SwiftAgentISH.xcframeworkfor the vendored iSH native library.ios_systemauxiliary XCFrameworks.
Shipping SwiftAgent itself as one giant XCFramework would not remove the hard part: Python still needs a final app-bundle processing phase so native extension modules are copied and signed as frameworks. SPM keeps those moving pieces visible and versioned while still letting app code say import SwiftAgent.
The sample app does the same thing a consuming app should do:
- The app target links the
SwiftAgentproduct. - The app target runs
Scripts/swiftagent-install-hermes.sh, so the bundle contains the Python runtime and Hermes payload. - The sample app's chat path uses
HermesAgent(configuration:), which runs in process.
SwiftAgent includes a reusable Xcode build phase helper:
set -euo pipefail
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/SwiftAgent/Scripts/swiftagent-install-hermes.sh"For local development against this repo, the sample app uses:
set -euo pipefail
"$PROJECT_DIR/../../Scripts/swiftagent-install-hermes.sh"The script ensures the pinned Hermes source exists, copies the Hermes Python payload into the final app bundle, overlays the platform-specific Python packages, and runs BeeWare's install_python helper to copy the Python standard library and convert .so extension modules into signed app frameworks. During staging, it strips Python bytecode caches and common source-control/build-tool cache directories. Hermes source files remain the runtime source of truth; bytecode precompilation is opt-in with SWIFTAGENT_PRECOMPILE_HERMES_PYTHON=YES.
By default the script uses Payloads/Hermes/PythonApp. If Payloads/Hermes/PythonApp/hermes is missing, it runs Scripts/update-hermes.sh to fetch the pinned source before copying the payload. Apps can set SWIFTAGENT_PYTHON_APP_SOURCE to their own payload directory and SWIFTAGENT_PYTHON_XCFRAMEWORK to a custom Python framework path. Set SWIFTAGENT_AUTO_FETCH_HERMES=NO to make missing Hermes source a hard build error.
Third-party Python packages are staged with this layout:
PythonApp/site-packages: pure Python/common dependencies.PythonApp/site-packages-iphonesimulator: simulator-native wheels.PythonApp/site-packages-iphoneos: device-native wheels.
SwiftPM cannot silently add this final app-bundle processing step to a consuming iOS app, so a small explicit Run Script phase is still required for the Python/Hermes backend. The app-facing Swift code stays at import SwiftAgent.
Local MLX support lives in the separate Packages/SwiftAgentMLX add-on package. The main SwiftAgent package intentionally has no MLX, Hugging Face, or tokenizer package dependencies, so hosted-model apps do not resolve that graph. Apps that want offline local-model experiments can add the add-on package and inject SwiftAgentMLXModelProvider explicitly.
Apple Foundation Models support lives in the separate Packages/SwiftAgentFoundationModels add-on package. It is weak-linked and guarded behind iOS/macOS/visionOS 26 availability. The provider uses the existing Hermes local-model bridge, presents compact native Foundation Models tool schemas for read_file, write_file, and terminal, and returns OpenAI-style tool calls to Hermes for execution.
Hermes is intentionally not a Git submodule and is not checked into this repository. The build phase fetches the pinned release on demand when the generated payload is missing. CI can either pre-run ./Scripts/update-hermes.sh for an explicit bootstrap step or set SWIFTAGENT_AUTO_FETCH_HERMES=NO to prevent network access during builds.
The source pin lives in Vendor/hermes-agent.lock:
HERMES_REPOSITORY: upstream Git repository.HERMES_TAG: reviewed upstream release tag.HERMES_VERSION: expected Python package version.HERMES_COMMIT: peeled release-tag commit.
To update Hermes:
- Review the upstream release.
- Update
Vendor/hermes-agent.lockto the new tag/version/commit. - Run
./Scripts/update-hermes.sh. - Run the host tests and simulator build.
Scripts/update-hermes.sh fetches the tagged source into Build/hermes-agent-src, verifies that the tag resolves to the pinned commit, verifies pyproject.toml matches the pinned version, and replaces only the ignored generated directory at Payloads/Hermes/PythonApp/hermes. It stages runtime-relevant Hermes files while excluding obvious upstream repo furniture like CI config, Docker/Nix files, tests, website docs, and release-note archives. It leaves the pure-Python and platform-native dependency layers in place. The script uses a simple lock directory so parallel app/extension build phases do not race while generating the payload.
The native wheels currently needed for OpenAI/Pydantic are jiter==0.13.0 and pydantic_core==2.41.5.
./Scripts/build-native-wheels.shThat builds simulator and device wheels into Build/wheelhouse. Updating the checked-in package layers still needs a vendor step: unzip the iphonesimulator wheels into Payloads/Hermes/PythonApp/site-packages-iphonesimulator and the iphoneos wheels into Payloads/Hermes/PythonApp/site-packages-iphoneos.
The iSH integration is session-based. A one-shot ish /bin/sh -c ... style runner works once, but is not reentrant in a single host process because the iSH kernel keeps global state. SwiftAgent instead boots one long-lived guest shell, writes commands over a pipe, and reads output until a private completion marker.
The current rootfs is copied from the app bundle into Application Support before first use because iSH mutates its fakefs metadata and the /workspace bind mount. This is packaging-heavy but keeps the POC honest: it runs real guest binaries rather than a hand-written command parser.
The iSH guest bind-mounts the SwiftAgent workspace at /workspace. Hermes file tools run through a direct iOS host-file bridge constrained to the same workspace, so shell-created files and Python-created files are visible to each other without pretending host absolute paths exist inside the guest.
Verified in simulator and generic iOS builds:
- Embedded CPython initializes from the app bundle.
- The app imports SwiftAgent bootstrap resources from the Swift package bundle.
- The app imports real Hermes
run_agent.pyfrom bundled source. - Hermes chat works with OpenAI-compatible endpoints and streams reasoning, tool calls, tool outputs, timing, and final responses back to Swift.
- Hermes memory/context/soul are enabled with persistent
HERMES_HOMEunder Application Support. - Hermes terminal calls route into a persistent iSH ARM64 Alpine shell session.
- The iSH guest bind-mounts the SwiftAgent workspace at
/workspace, so shell-created files are visible to Python/file tooling. - The bundled iSH rootfs includes
python3,rg,jq, andgitfor a first useful agent shell POC. - A local MLX/Qwen 2B provider can be wired through the optional add-on package and the same model-provider bridge as an offline proof of concept.
- Apple Foundation Models can be wired through the optional add-on package. On an iPhone 17 Pro Max, direct Hermes chat returned first text in about 5 seconds, and a basic file-tool run caused the model to request
write_file,read_file, andterminalcalls that Hermes executed.
- Hermes is still the only agent implementation. The package shape is ready for more, but the generic agent API is intentionally thin until a second implementation proves it.
- Many desktop-style tools remain inappropriate for iOS: browser automation, MCP stdio servers, runtime package installation outside the guest rootfs, and desktop computer-use.
- The iSH backend currently supports one embedded shell session per process. Multiple concurrent sessions need more invasive iSH state isolation.
- The package-level
HermesAgent(configuration:)convenience initializer runs in process. - The bundled full Alpine fakefs is large; a distributable package should eventually build a smaller purpose-made rootfs.
- The optional local MLX model provider is a POC. The 2B model can run offline, but it is weak at tool use compared with a hosted model.
- The optional Foundation Models provider is a POC. It is much faster and more memory-stable than MLX, but Hermes' generic OpenAI-style loop is not an ideal fit for Apple's small context window and native tool design. A first-class SwiftAgent-native Foundation Models agent is likely cleaner than pushing this through every Hermes feature.
- Generic
iphoneosbuild can be verified withCODE_SIGNING_ALLOWED=NO; real device install still needs normal Apple signing/provisioning.