Runtime progress, tools, and turn control

September 10, 2026 · View on GitHub

Documentation index · CodexKit

CodexKit supports these Codex runtime concepts natively in Swift. First-class question/form presentation remains host-owned.

GPT-6 Astra

let backend = CodexResponsesBackend(configuration: .init(model: .gpt6Astra))
let threadConfiguration = AgentThreadConfiguration(model: .gpt6Astra)

The bundled Astra identifier is gpt-6-astra. Its default reasoning effort is .low, and supported efforts are .low, .medium, .high, .extraHigh, .max, and .ultra. As with the existing models, .ultra is sent as max to inference; it does not add agent delegation to CodexKit. The bundled context-window value is 272,000 tokens, matching upstream's default window, rather than its optional larger maximum. Model selection does not grant account access.

Stream completion

The built-in Responses backend requires response.completed. A stream that ends without that event throws responses_stream_disconnected. Existing retry policy still decides whether a retry is safe: emitted text, progress, structured output, committed assistant messages, and tool calls prevent automatic replay. Before output, a retry sends the same POST again; it does not resume the original provider response. See the recovery investigation.

The configured streamIdleTimeout is also applied to the Responses URL request. Custom AgentBackend implementations retain their existing completion behavior.

Parallel tools

Tools execute serially by default. Opt in for independent tools:

let lookup = ToolDefinition(
    name: "lookup_product",
    description: "Read product details.",
    inputSchema: .object([
        "type": .string("object"),
        "properties": .object([:])
    ]),
    supportsParallelExecution: true
)

AgentRuntime.Configuration.maximumParallelToolCalls bounds each batch; the default is four, and values below one are clamped to one. The Responses backend requests parallel calls when at least one registered tool opts in. It collects the response's calls and emits AgentBackendEvent.toolCallsRequested.

The runtime executes consecutive eligible calls together. A serial tool or an approval-gated tool is a barrier: preceding work finishes before it runs, and following work waits for it. Approval-gated tools remain exclusive even if their parallel flag is true. Turns with skill tool-policy constraints run tools serially to preserve call limits and sequencing.

Tool lifecycle events identify individual calls and may finish out of order. Provider call/result history retains the model's original call order. The thread's pending snapshot represents one outstanding wait; use tool lifecycle events to display all concurrent calls. Host tools remain responsible for their own resource synchronization and cooperative cancellation.

Custom backends can continue emitting toolCallRequested for one call, or emit toolCallsRequested for a batch and accept results by invocation ID. The existing AgentTurnStream(events:submitToolResult:) initializer remains available.

Every tool result must preserve the requested invocation ID and tool name. If a custom executor returns a different identity, the runtime records a failed result for the original call before saving history or publishing the result.

Direct Responses backend callers must submit one result per announced call. Out-of-order results for an announced batch are supported. Unknown, duplicate, mismatched, and late submissions throw AgentRuntimeError with code invalid_tool_result, without replacing a previously accepted result. Completion and cancellation release outstanding result buffers and waiters. The shared AgentTurnStream API also checks that the envelope ID matches its submission ID; custom backends remain responsible for tracking their own pending calls.

All nonempty tool-result text blocks are joined in order with blank-line separators for provider requests, fallback replies, and compaction context. primaryText continues to expose the first block for concise previews.

Remote tool images require a successful HTTP response and a decodable PNG, JPEG, GIF, WebP, HEIC, or HEIF payload. The image bytes determine the media type, even if the server omits or mislabels it. HTTP errors, HTML, and truncated image data are omitted from attachments. Existing download byte limits and cancellation still apply. Validation leaves accepted bytes unchanged; SQLite and Realm retain attachment references while the original bytes remain in disk blobs.

Progress and message phases

Both ordinary and structured streams expose:

  • .progress(AgentTurnProgress) for message start/completion, reasoning-summary deltas, and web-search activity.
  • .rateLimitsUpdated([AgentRateLimitSnapshot]) for account limit changes.
  • .turnInterrupted(AgentTurnInterruption) for cancellation.

AgentMessage.phase distinguishes .commentary and .finalAnswer when the provider supplies them. It is optional, persists across all storage adapters, and preserves unknown future phase strings. Existing assistantMessageDelta events remain answer-text events; reasoning summaries are separate.

Enable provider reasoning summaries with CodexResponsesBackendConfiguration(enableReasoningSummaries: true). The backend requests reasoning.summary = "auto"; events depend on what the model actually supplies. Raw private reasoning is not exposed.

CodexKitUI.AgentRuntimeStore exposes latestProgress and rateLimits, and provides steer(_:) and interrupt() helpers for the selected thread.

Model discovery

let catalog = try await runtime.listModels()
let choices = catalog.visibleModels

// Require a successful network refresh:
let refreshed = try await runtime.listModels(policy: .refresh)

// Avoid network I/O:
let offline = try await runtime.listModels(policy: .cachedOnly)

Discovery fetches the Codex /models catalog with the signed-in session. It returns model identifiers, descriptions, reasoning efforts, modalities, context windows, visibility, and parallel-tool capability where supplied. Unknown model identifiers and reasoning efforts remain usable.

The backend keeps an in-memory cache per account with a five-minute freshness window and ETag revalidation. .preferCached uses a fresh cache, otherwise refreshes, falling back to stale or bundled metadata for non-authentication failures. .refresh reports failures; authentication failures always propagate. The snapshot exposes source, fetchedAt, and isStale. This cache lasts for the backend instance's lifetime. Refreshing model metadata also updates its context-window lookup for the selected account.

The discovery request's client_version defaults to 0.153.0; override modelClientVersion in backend configuration when targeting another server compatibility version. Backends without AgentBackendModelDiscovering return the bundled catalog through the runtime facade.

Account limits

let limits = try await runtime.rateLimits()
for limit in limits {
    if let window = limit.primary {
        print(window.remainingPercent, window.resetsAt as Any)
    }
}

These are the latest observed snapshots for the signed-in account, not a new quota request. The backend reads limits from HTTP headers, including error responses, and codex.rate_limits events. Multiple metered limit families are kept separately. Primary/secondary windows and credit information are optional; missing values mean unavailable. AgentUsage continues to describe turn token usage separately.

Steering and interruption

Capture the turn ID from .turnStarted or runtime.activeTurnID(in:):

try await runtime.steer(
    "Focus on recent results.",
    in: threadID,
    expectedTurnID: turnID
)

try await runtime.interrupt(in: threadID, expectedTurnID: turnID)

Steering accepts text and optional images. It queues input for the next model request within the same turn, including an additional request if the current response would otherwise finish. It cannot change an HTTP response already being generated. Acceptance is serialized with the backend's completion decision; stale turn IDs and completed turns are rejected. The user message is committed when the backend consumes it. Model, persona, and response-format overrides still belong to a new turn.

A thread accepts one persistent active turn at a time. A second stream call throws thread_busy; ephemeral requests remain independent. Custom backends can opt into steering and explicit interruption with the extended AgentTurnStream initializer. Steering unsupported by a backend throws steering_unsupported.

Interruption releases built-in pending tool waits and cancels the turn task. The approval inbox also cancels pending presentation. Host executors and custom approval presenters should cooperate with task cancellation. An interrupted turn records .interrupted as its latest turn status, clears pending state, returns the thread to .idle, emits .turnInterrupted, and ends the stream with CancellationError. It does not emit successful completion or capture completion memory attribution. Already completed external actions are not undone.

Demo walkthrough

The checked-in iOS demo uses account discovery for its model picker, including Astra when available, and displays catalog provenance and reported account limits. Its ordinary chat screen shows provider progress and message phases, with Add to turn and Stop controls. Parallel Lookups requests two independent sample tools and displays observed concurrency. The interactive backend enables reasoning summaries; model output still determines whether summaries arrive.

See Try the runtime features for steps, expected behavior, and the distinction between live account data and fixed sample tool outputs.

Migration

Update exhaustive switches for the added event cases, AgentTurnStatus.interrupted, and AgentSystemEventType.turnInterrupted. Cancellation now produces interrupted status rather than failed status. Existing tools stay serial unless explicitly opted in, and existing stored messages decode with phase == nil.