SwiftASB is a Swift library package for driving the Codex app-server from Swift.
SwiftASB is in early development, and v0.0.1 is the current experimental baseline.
SwiftASB is a library-first Swift package that wraps the Codex app-server lifecycle in typed Swift APIs. The current public surface centers on CodexAppServer, CodexThread, and CodexTurnHandle, with typed initialize, thread start, turn start, and event-stream handling for Swift clients that want to work against a local Codex CLI runtime.
This package exists to give Swift developers a Swift-native bridge to the Codex app-server without exposing generated wire types as the public API. The repo is maintained separately so transport, protocol typing, concurrency behavior, live subprocess verification, and public package ergonomics can be worked out in the open as a real Swift library instead of as one-off glue code.
SwiftASB is still experimental, but the shortest real path to trying it is:
- Install the local Codex CLI and make sure
codexis on yourPATH, or plan to pass an explicit executable URL inCodexAppServer.Configuration. - Add the package to your SwiftPM project.
- Initialize the client, start a thread, and start a turn.
.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "0.0.1")If you just want to explore the package repo itself, start with the commands in Development.
The package assumes a local Codex CLI runtime. The currently shipped public surface includes:
CodexAppServerfor process ownership, initialize, thread start,thread/list,thread/read,thread/resume,thread/fork, paged turn-history reads, app-wide model and MCP-server status listing, and turn start.CodexThreadfor thread-scoped turn creation plus a liveDashboardcompanion.CodexTurnHandlefor typed turn events plus a liveMinimapcompanion.- typed approval and elicitation request models, with explicit response APIs on
CodexThreadandCodexTurnHandle.
The current public lifecycle contract is intentionally narrow and explicit:
CodexAppServerowns the local subprocess plus initialize, thread start,thread/list,thread/read,thread/resume,thread/fork,thread/turns/list,model/list,mcpServerStatus/list, and turn start.CodexAppServer.listModels(...)andCodexAppServer.listMcpServerStatuses(...)expose app-wide capability snapshots that are not owned by one conversation thread.CodexThreadowns thread-scoped turn creation and thread-scoped fallback responses for unroutable interactive requests.CodexTurnHandleowns the active turn stream plus turn-scoped control methods, includingrespond(to:with:),steer(_:),steerText(_:), andinterrupt().CodexTurnEventandCodexThreadEventsurface typed progress, item, approval, elicitation, and request-resolution events without exposing raw generated wire payloads.DashboardandMinimapare current-state mirrors of the typed public event streams rather than a second control path.CodexTurnHandle.Minimap.callSnapshotsalready gives a stable per-turn view of command, file-edit, dynamic-tool, collab-tool, and MCP activity.CodexTurnHandle.Minimapnow also exposesisCompactingThreadContextso per-turn UI can show when context compaction is actively running.CodexThread.Dashboardalready summarizes aggregate tool activity, aggregate MCP activity, active hook runs plus their latest live status, and whether thread compaction is currently active.CodexThread.compactContext()now wrapsthread/compact/startdirectly.CodexThread.makeRecentTurns(limit:)now vends a thread-scoped recent-turns observable that prewarms from the local history store, supports explicit older/newer whole-turn window expansion, seeds upstream paging cursors even when the visible initial window came from local history, and can fall back to paged stored-turn reads when local recent history is not resident yet.CodexThread.RecentTurnsnow owns a first-pass in-memory cache policy too: it surfaces load-state flags, trims its resident turn window around bound scroll or visibility context, tracks both resident item counts and weighted resident item cost, ships named presets for chat UIs, full transcript or inspector UIs, and compact history rails, slims low-value item payloads out of older non-visible completed turns before evicting whole turns, rehydrates slimmed turns when they become visible again, and can automatically prefetch older or newer windows when a SwiftUI consumer binds the visible turn id throughscrollPosition(id:anchor:)and visibility throughonScrollTargetVisibilityChange(idType:threshold:_:).CodexThread.makeRecentFiles(limit:)now vends a thread-scoped recent-files observable that is file-centric rather than event-centric: it hydrates from persisted file-change items in the local history store, keeps one resident entry per file-change item, enriches live entries from file-change output deltas, can load older file entries from the same turn before moving on to older turns, and now supports selection-aware shell-versus-payload slimming plus automatic payload rehydration when a protected file becomes visible or selected again. Its resident payload pressure now accounts for diff structure and line volume, and its retained shell summary now prefers concise edit summaries such as additions, deletions, and hunk counts over raw status strings when sealed payload is available.CodexThread.makeRecentCommands(limit:)now vends a thread-scoped recent-commands observable that is command-centric rather than event-centric: it hydrates from persistedcommandExecutionitems in the local history store, keeps one resident entry per command item, enriches live entries from command-output deltas, can load older command entries from the same turn before moving on to older turns, and now supports selection-aware shell-versus-output slimming plus automatic output rehydration when a protected command becomes visible or selected again. Its shell summaries prefer command status and concise output summaries, and its resident output pressure is weighted by output size and line structure rather than by raw entry count alone.CodexThread.HistoryWindownow gives non-UI callers a lightweight thread-scoped page shape with sealedClosedTurnvalues plushasOlderTurnsandhasNewerTurns.CodexThread.readTurnHistory(turnID:),readRecentTurnHistoryWindow(limit:),readOlderTurnHistoryWindow(olderThan:limit:), andreadNewerTurnHistoryWindow(newerThan:limit:)now expose the first deliberate non-UI local-history reads, while the matching array-returning helpers remain convenience wrappers over those windows.CodexTurnHandle.close()now seals a completed turn into a caller-owned value snapshot and releases per-turn observation bookkeeping explicitly.thread/read(includeTurns: true)andthread/turns/list(...)now hydrate the internal history store so stored-thread reads can enrich the same local persistence layer as live item-stream assembly.thread/resume(...)now restores thread defaults, clears stale archived state for the reopened thread, and hydrates any resumed persisted turns back into that same local history store instead of treating a resumed thread like a fresh conversation.thread/fork(...)now creates typed forked-thread sessions, persists copied fork history into thread-scoped local turn records, and records both the source thread id and the last shared turn id as explicit local lineage data.thread/list(...)now returns typed stored-thread pages and reconciles local thread metadata and archive state from list results, which gives the package a first list-driven path for archive-drift correction.- overlapping stored-history hydration now reconciles against live-built local turns instead of blindly overwriting them, preserving richer local item detail when upstream stored history is thinner while still accepting canonical terminal status from upstream.
- the internal history store now tracks conservative completeness state for each
thread, with
serverParityfor clean stored-history hydration andricherThanServerwhen local item-stream assembly has preserved detail that upstream stored reads did not return.
Current concurrency behavior is also explicit:
- Different threads may host concurrent turns.
- Overlapping turns on the same thread are rejected client-side with
CodexAppServerError.invalidStatebecause the live app-server does not yet expose a reliable independent lifecycle for them.
Current non-goals and intentionally deferred areas are also explicit:
- The generated wire layer stays internal.
- There is not yet a one-shot
run(...)convenience API. - The current history-reading API is still intentionally narrow: the package now exposes direct thread-scoped reads for one sealed turn plus lightweight local history windows around recent or boundary-based queries, but there is not yet a broader public search or consumer-friendly cursor helper surface over the local store.
- The broader history-reading API is still intentionally incomplete even though
RecentTurns, explicitCodexTurnHandle.close(), and the first non-UIClosedTurnhelpers now exist: there is not yet a fuller public cursor model, search surface, or scroll-driven history-window API over the local store. - The current reconciliation policy is intentionally conservative and still internal: merge rules now distinguish terminal status from richer local text and command detail, and the package now persists explicit fork lineage plus thread-scoped copied fork history, but a broader public history-reading API is still open.
- Raw command-output and file-change-output deltas still remain internal as
transport detail, but they now feed
RecentCommandsandRecentFilesrespectively instead of becoming new top-level public event cases. Richer MCP-progress detail still remains internal while the package decides whether that belongs as new event cases or as deeper observable summary state. - Model reroute notifications remain internal and are currently logged operationally rather than exposed as a public lifecycle surface.
- The live approval-path probe is best-effort runtime observation, not a deterministic release gate, because the current Codex runtime does not reliably force an approval request on command.
Current Codex CLI compatibility policy is intentionally rolling:
SwiftASBaims to support the latest public Codex CLI release plus the prior two minor versions.- The current documented window is
0.122.xthrough0.124.x. - When newer Codex CLI releases add protocol features without breaking existing
behavior,
SwiftASBmay adopt those additions later or gate them behind newer-version-aware promotion work rather than treating every new upstream feature as an immediate public-surface requirement. - If Codex reaches a future major-version release with a materially different compatibility story, this policy should be reassessed rather than assumed to carry forward unchanged.
Current executable discovery on macOS follows this order when you do not pass
an explicit CodexAppServer.Configuration.codexExecutableURL:
- probe
codex --versionthrough the currentPATH - check the common Homebrew install locations
/opt/homebrew/bin/codexand/usr/local/bin/codex - check the npm global prefix reported by
npm prefix -g, then look for<prefix>/bin/codex
If you want to bypass discovery entirely, pass an explicit executable URL.
After start(), you can inspect the resolved binary, version string, and
documented support-window assessment through
CodexAppServer.cliExecutableDiagnostics().
A minimal flow looks like this:
import SwiftASB
let client = CodexAppServer()
try await client.start()
defer { Task { await client.stop() } }
_ = try await client.initialize(
.init(
clientInfo: .init(
name: "ExampleApp",
title: "Example App",
version: "0.1.0"
)
)
)
let thread = try await client.startThread(
.init(
approvalPolicy: .never,
currentDirectoryPath: "/absolute/path/to/workspace",
ephemeral: true,
sandboxMode: .workspaceWrite
)
)
let turn = try await thread.startTextTurn(
"Reply with exactly: hello from SwiftASB",
approvalPolicy: .never,
summary: .none
)
for try await event in turn.events {
if case let .completed(completion) = event {
print(completion.turn.status)
break
}
}This example shows the current intended handle-owned lifecycle: start a turn, observe events, answer approval requests if they appear, and optionally steer or interrupt the active turn.
import SwiftASB
let client = CodexAppServer()
try await client.start()
defer { Task { await client.stop() } }
_ = try await client.initialize(
.init(
clientInfo: .init(
name: "ExampleApp",
title: "Example App",
version: "0.1.0"
)
)
)
let thread = try await client.startThread(
.init(
approvalPolicy: .onRequest,
approvalsReviewer: .user,
currentDirectoryPath: "/absolute/path/to/workspace",
ephemeral: true,
sandboxMode: .workspaceWrite
)
)
let turn = try await thread.startTextTurn(
"Inspect the workspace and summarize what changed.",
approvalPolicy: .onRequest,
summary: .concise
)
try await turn.steerText("Keep the answer short and lead with the most important change.")
for try await event in turn.events {
switch event {
case let .approvalRequested(request):
switch request {
case .commandExecution:
try await turn.respond(to: request, with: .commandExecution(.accept))
case .fileChange:
try await turn.respond(to: request, with: .fileChange(.accept))
case let .permissions(permissionsRequest):
try await turn.respond(
to: request,
with: .permissions(
.init(
permissions: permissionsRequest.permissions,
scope: .turn
)
)
)
}
case let .elicitationRequested(request):
switch request {
case let .toolUserInput(inputRequest):
let answers = Dictionary(
uniqueKeysWithValues: inputRequest.questions.map { question in
(question.id, CodexToolUserInputResponse.Answer(answers: []))
}
)
try await turn.respond(
to: request,
with: .toolUserInput(.init(answers: answers))
)
case .mcpServer:
try await turn.respond(
to: request,
with: .mcpServer(.init(action: .accept))
)
}
case let .serverRequestResolved(resolution):
print("Resolved request:", resolution.requestID)
case let .completed(completion):
print("Turn finished with status:", completion.turn.status)
break
default:
continue
}
}
// If your UI or workflow decides the turn should stop early:
try await turn.interrupt()If the runtime does not naturally raise an approval request for a particular prompt, that is expected today. The live approval-path test is useful for observing current behavior, but it is not treated as deterministic release coverage.
- Use Swift 6.3 or newer on macOS 15 or newer.
- Install the local Codex CLI if you want to use the package against a real runtime or run the live integration tests.
- Clone the repo and build with SwiftPM.
swift build- Keep the public API deliberate and library-first.
- Use the fake transport tests for deterministic public-surface work.
- Use the opt-in live tests when verifying real Codex CLI subprocess behavior.
- Treat the live approval-path probe as best-effort coverage, not as a deterministic release gate; the current Codex runtime does not reliably force an approval request on command, so the same prompt may either raise approval or complete directly.
- Keep generated wire code internal and treat the public wrappers as the actual package surface.
- Keep temporary codegen artifacts under
codex-schemas/andtmp/untracked unless a maintainer explicitly decides otherwise.
Run these first:
swift build
swift testUseful opt-in live checks:
env SWIFTASB_ENABLE_LIVE_CODEX_TRANSPORT_TESTS=1 swift test
env SWIFTASB_ENABLE_LIVE_CODEX_SINGLE_TURN_TESTS=1 swift test
env SWIFTASB_ENABLE_LIVE_CODEX_CROSS_THREAD_TESTS=1 swift test
env SWIFTASB_ENABLE_LIVE_CODEX_APPROVAL_TESTS=1 swift test
env SWIFTASB_ENABLE_LIVE_CODEX_SAME_THREAD_TESTS=1 swift testThose live suites launch the local Codex CLI through temp workspaces with explicit test time limits. They are intentionally opt-in so day-to-day package validation stays fast and deterministic. The approval-path probe is especially non-deterministic today and should be read as runtime observation rather than a strict pass/fail release gate.
.
├── Package.swift
├── README.md
├── ROADMAP.md
├── Sources/
│ └── SwiftASB/
│ ├── Generated/
│ ├── Protocol/
│ ├── Public/
│ └── Transport/
├── Tests/
│ └── SwiftASBTests/
│ ├── Protocol/
│ ├── Public/
│ └── Transport/
└── scripts/
ROADMAP.md tracks milestone status and the next release-facing work. v0.0.1 is already tagged as the current experimental baseline, and the roadmap plus git history remain the source of truth for what has shipped versus what is still intentionally open.
SwiftASB is licensed under FSL-1.1-ALv2. That means current versions are
available under the Functional Source License with no commercial competing-use
right, and each version converts to Apache 2.0 on the second anniversary of
the date that version was first made available. See LICENSE.