Add Swift SDK#1512
Conversation
Co-Authored-By: paduh <paduh@users.noreply.github.com>
|
@paduh is attempting to deploy a commit to the CopilotKit Team on Vercel. A member of the Team first needs to authorize it. |
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen install the packages you need: # Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1776107649' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1776107649' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1776107649' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1776107649' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1776107649' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1776107649' --index testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1776107649
Commit: 6bd7ac7 |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/middleware-starter
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/aws-strands
@ag-ui/agno
@ag-ui/claude-agent-sdk
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/langroid
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/server-starter
@ag-ui/server-starter-all-features
@ag-ui/vercel-ai-sdk
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
jpr5
left a comment
There was a problem hiding this comment.
Review: Add Swift SDK (PR #1512)
Thanks for this contribution, @paduh — this is a substantial piece of work and it's clear you have real Swift expertise. The 4-module architecture is clean, the actor usage is solid, the value-type event model with Sendable conformance is the right call, and the ChatApp example is one of the better community SDK demos I've seen. The shared test protocols (EventDecodingErrorTests) that eliminate duplication across event test files are particularly elegant.
That said, this review surfaced a number of issues that need to be addressed before this can merge. I've organized them by severity — the Critical and High sections are blocking, Medium items are strong recommendations, and the rest is polish.
Critical — Must Fix Before Merge
C1. 7 Reasoning event types are completely missing
The AG-UI protocol defines 7 REASONING_* events that are the non-deprecated replacements for the THINKING_* events:
REASONING_STARTREASONING_MESSAGE_STARTREASONING_MESSAGE_CONTENTREASONING_MESSAGE_ENDREASONING_MESSAGE_CHUNKREASONING_ENDREASONING_ENCRYPTED_VALUE
The Swift SDK only implements the deprecated THINKING_* events. The Go community SDK (the most up-to-date reference) has all 7 Reasoning events. Without these, the Swift SDK cannot interoperate with agents that send Reasoning events, which is the current protocol direction.
What to do: Add all 7 event types to EventType.swift, create corresponding event structs, DTOs, and registry entries. Also add ReasoningMessage and the reasoning case to the Role enum (see C2).
Reference: sdks/typescript/src/types/events.ts lines 54-61, sdks/community/go/core/events.go lines 56-68.
C2. ReasoningMessage type and reasoning Role are missing
The TypeScript protocol defines ReasoningMessage with role: "reasoning", content: string, and encryptedValue: string?. The Swift Role enum (Sources/AGUICore/Types/Messages/Role.swift) has developer, system, assistant, user, tool, activity — but no reasoning.
Additionally, encryptedValue: String? is missing from all message types and from ToolCall. Grep confirms zero occurrences of "encryptedValue" in the entire SDK. This is a protocol-wide field defined on BaseMessageSchema (TypeScript types.ts line 20) and ToolCallSchema (line 12).
What to do: Add .reasoning to Role, create ReasoningMessage struct, add encryptedValue: String? to the Message protocol and all concrete message types, and to ToolCall.
High — Blocking Issues
Protocol Conformance
H1-H3. Lifecycle event field discrepancies
| # | Event | Issue |
|---|---|---|
| H1 | RunStartedEvent |
Missing parentRunId and input fields (needed for nested runs) |
| H2 | RunFinishedEvent |
Missing result field (z.any().optional() in TS spec) |
| H3 | RunErrorEvent |
Non-standard: extra threadId/runId, nested ErrorInfo. Protocol has flat message/code only. Will cause deserialization failures. |
H4. RawEvent DTO reads wrong wire field name + missing source
RawEventDTO reads jsonObject["data"] from the wire, but the protocol field is event (TypeScript events.ts line 202: event: z.any()). This means a protocol-conforming RawEvent payload like {"type":"RAW","event":{...}} will fail to decode — the DTO looks for "data", not "event". The Kotlin SDK correctly reads event. Additionally, the protocol defines source: z.string().optional() which is entirely absent.
| Field | Protocol (TS) | Swift DTO reads | Kotlin SDK |
|---|---|---|---|
| event | event: z.any() |
"data" (wrong key) |
event: JsonElement |
| source | source: z.string().optional() |
missing | source: String? |
What to do: Change RawEventDTO to read jsonObject["event"], add source: String? to RawEvent.
Correctness Bugs
H5. Bool/Int type priority bug in JSON encoding (Darwin)
In Sources/AGUICore/Utilities/JSONCodingHelpers.swift, the encoding path (line ~148) checks as? Int and as? Double before as? Bool. On Darwin platforms, Swift's Bool bridges to NSNumber, so NSNumber(value: true) as? Int evaluates to 1. This means boolean values in state snapshots and custom events can be silently encoded as integers.
Note: the decoding path (lines 71-84) has Int before Bool in the try-order, but Foundation's JSONDecoder (since Swift 4.2) correctly distinguishes JSON booleans from integers, so the decode side is safe in practice. The PatchApplicator.swift (lines 481-488) correctly orders Bool before Int — this file should match that pattern.
What to do: In encodeJSONObject/encodeJSONArray, move the Bool check before Int/Double. Consider also reordering the decode side for defensive correctness.
H6. SSE parser doesn't handle \r\n line endings
Sources/AGUIClient/Streaming/SseParser.swift line 120 splits on "\n\n" only. The SSE spec (WHATWG) requires handling \r\n, \r, and \n as line endings. A server sending \r\n\r\n as the event terminator (common behind Windows servers or certain proxies) will never trigger event dispatch — the parser will buffer indefinitely.
The code even has a comment acknowledging this but doesn't implement it.
H7. Duplicate assistant messages in conversation history on multi-tool-call sequences
Sources/AGUIAgentSDK/StatefulAgUiAgent.swift lines 448-454: In trackHistoryAndState, each ToolCallEndEvent appends the current currentAssistantMessage snapshot to history. If an assistant message has N tool calls (ToolCallStart → ToolCallEnd → ToolCallStart → ToolCallEnd), the assistant message gets appended N times.
Additionally, currentAssistantMessage is not reset after ToolCallEndEvent, so on TextMessageEndEvent for the same message, it would be appended again.
This corrupts conversation history sent to the server on subsequent turns.
H8. JSON Pointer "/" path handled incorrectly in PatchApplicator
Sources/AGUIClient/State/PatchApplicator.swift lines 342-344: The path "/" returns [] (empty tokens), which operations interpret as "the root itself." Per RFC 6901, "/" points to the empty-string key "" in an object — the root is represented by the empty string "", not "/".
This means {"op": "add", "path": "/", "value": 42} replaces the entire document instead of setting the "" key. The test testAddToRootPath documents this incorrect behavior as expected.
H9. withTimeout helper always reports "unknown" tool name
Sources/AGUITools/Registry/ToolRegistry.swift line 335:
throw ToolExecutionError.timeout(toolName: "unknown", duration: duration)The actual tool name is available at the call site (line 229) but is not passed to withTimeout. All timeout errors will say "unknown tool," making debugging impossible.
H10. HTTPResponse type coupled to URLSession.AsyncBytes defeats HTTPClient abstraction
Sources/AGUIClient/Transport/HTTPClient.swift: The HTTPResponse struct returns URLSession.AsyncBytes, which means every consumer is locked to URLSession. A mock HTTP client must produce URLSession.AsyncBytes, which is practically impossible without a real URLSession. This undermines the entire purpose of the HTTPClient protocol abstraction.
Suggestion: Use AsyncThrowingStream<UInt8, Error> or a custom AsyncSequence wrapper as the byte stream type.
Design
H11. 5 config types with ~80% field overlap
| Type | Module | Used By |
|---|---|---|
AgentConfig |
AGUIClient | Nothing externally |
HttpAgentConfig |
AGUIClient | Nothing externally |
HttpAgentConfiguration |
AGUIClient | HttpTransport |
AgUiAgentConfig |
AGUIAgentSDK | AgUiAgent |
StatefulAgUiAgentConfig |
AGUIAgentSDK | StatefulAgUiAgent |
AgentConfig and HttpAgentConfig appear unused by the high-level API. AgUiAgentConfig and StatefulAgUiAgentConfig duplicate ~80% of their fields. bearerToken has a didSet that mutates headers["Authorization"] in some configs but not others — and the didSet doesn't fire from init.
Suggestion: Consolidate to 2 config types max.
H12. DTO layer is 48 files — mostly unnecessary indirection
48 Swift files in Sources/AGUICore/Decoding/. The DTO layer serves a legitimate purpose for events that need wire-to-domain field mapping (e.g., CustomEventDTO maps name→customType) or require JSONSerialization for untyped payloads. But for the ~15 simple events where DTOs are 1:1 field copies with no transformation, the indirection adds maintenance cost without benefit.
Suggestion: Have simple domain types conform to Decodable directly. Keep DTOs for complex events where wire mapping or JSONSerialization is genuinely needed.
Test Gaps
H13-H15. Major untested components
| # | Component | Impact |
|---|---|---|
| H13 | StatefulAgUiAgent |
Most complex class in SDK — history, state, tool calls — zero tests. The duplicate-message bug (H7) would have been caught immediately. |
| H14 | ToolExecutionManager |
Event stream processing, tool call buffering, concurrent execution — zero tests |
| H15 | MessageEncoder |
Entire Encoding/ directory untested. No round-trip tests. Also: StepFinishedEvent has no test file. |
Medium — Strong Recommendations
Production Readiness
- No SSE reconnection strategy — connection drops silently terminate the stream. Important for mobile, but no other community SDK ships this either. At minimum, document the limitation prominently.
- No SwiftUI / Combine integration — only raw
AsyncThrowingStream. Production iOS apps need@Observablebridging. The Kotlin SDK provides coroutine Flow, but Go and Rust SDKs don't have framework-specific integration either. CustomEventdomain naming diverges from protocol — DTO correctly maps wire fieldsname→customTypeandvalue→data, so serialization works. But the domain-type naming confuses developers who know the protocol. Consider usingname/valuedirectly like the Kotlin SDK.
Protocol Gaps
TextMessageStartEventandTextMessageChunkEventmissingname: String?field — defined in TypeScript spec at events.ts lines 75, 94.- Missing multimodal
InputContenttypes — the protocol definesImageInputContent,AudioInputContent,VideoInputContent,DocumentInputContent. Only legacyBinaryInputContentis present. ActivityMessageusesactivityContent: Datainstead of protocol'scontent: Record<string, any>— both the type and field name differ from the wire format.@frozenonEventTypeenum (EventType.swift:41) — prevents adding new cases without ABI break. Premature for an evolving protocol. Remove it.UnknownEvent.eventTypereturns.raw— collides with actual RAW events. Consumers can't distinguish the two.Toolstruct missingmetadata: [String: Any]?field — used for A2UI schema and tool extensions.
Concurrency Concerns
- Multiple
@unchecked Sendableon mutable classes —AgUiAgent(open class — subclasses inherit the unsafe annotation),MutableToolExecutionStats,ToolCallBuilder. ConsiderstructforToolCallBuilderandfinalforAgUiAgent. .suspendbuffering strategy doesn't implement backpressure — behaves nearly identically to.dropOldest.TimeBatchedAsyncSequencedoesn't properly window by time — blocks on upstreamnext(), so the time check only fires after an element arrives.EventStreambyte-by-byte processing — triesString(bytes:encoding:.utf8)on every single byte. O(n²) for large responses.- Unstructured
Taskleaks in ChunkTransformer, EventVerifier, AgUiAgent.sendMessage, AgUiAgent.close, StatefulAgUiAgent — noonTerminationhandlers or cancellation checks. bearerTokendidSet doesn't fire frominitin AgentConfig and HttpAgentConfiguration — constructing with a bearerToken won't populate the Authorization header.- Subscriber execution order non-deterministic —
Dictionary.valuesarbitrary order.
Dead Code Posing as Features
RetryPolicydefined but never used — users can configure retry and it silently does nothing. Either implement or remove.ToolErrorHandler/CircuitBreakerfully disconnected from execution pipeline — retry strategies are dead code. Either wire them in or remove them.connectTimeoutonAgUiAgentConfig— declared but never passed to transport.
Other
ToolExecutionManagerswallowsToolRegistryErrordetail — always sends generic "Tool not available" error.ToolRegistryprotocol has 10 methods including stats — consider splitting into core registry + stats extension.- No logging strategy beyond
print(). Considerswift-logintegration. - Package.swift says iOS 16+ but README says iOS 15+ — one is wrong.
- CI only runs build+test on macos-latest — no SwiftLint, no multi-version, no Linux.
- ChatApp doesn't handle
StateSnapshotEvent/StateDeltaEvent— the core AG-UI shared-state mechanism is absent from the example. - ChatApp
buildAgenthas a race — creates agent, then Task creates a second with tools and reassigns. Messages can be lost. - Deprecated
onChange(of:)single-parameter form used in multiple views — deprecated in iOS 17.
Low / Nit — Non-Blocking Polish
25 Low + 14 Nit findings (click to expand)
Low
- No round-trip encode/decode tests
- No concurrency tests for decoder despite thread-safety claims
EndToEndPipelineTeststest wrong module (AGUIClient, not AGUIAgentSDK)- System message detection is positional-only in history trimming
- No token-based conversation trimming (only message count)
- No sliding window or summarization trimming strategies
- Encoding error misleadingly thrown as
ClientError.decodingError abortRun()/dispose()fire-and-forget with unstructuredTaskrunSubscribersWithMutationis a free function instead of a methodMockHTTPClientnever verifies request bodiestestStreamCanBeIteratedMultipleTimesis misleading — mock creates fresh iterator- No test for
trim(maxLength: 0)with a system message - Code duplication between
StatefulAgUiAgent's two initializers State = Datatype alias loses type safety- Duplicate
JSONPrimitiveWrapperacross DTO files - Double JSON parse on every decode (extract type, then full parse)
- Tool response handler silently discards server error events
- No accessibility labels in ChatApp (send button reads as "paperplane.fill")
Color(UIColor.systemBackground)limits ChatApp to iOS only- API key field uses
TextFieldnotSecureField MockAgUiAgentdefined but never used in tests- No test for
cancelStreaming()behavior - No test for A2UI
handleA2UIAction - Concurrency tests rely on timing (
Task.sleep(for: .milliseconds(10))) - License inconsistency: LICENSE says "2024 AGUISwift Contributors", source headers say "2025 Perfect Aduh"
Nit
- 23-line MIT license header on every file (convention: 1-line + LICENSE reference)
- Excessive doc comments restating code
- Dead
AGUICorestruct withcoreFunction()returning "AGUICore is working" - Deprecated
AGUIToolsstruct should be deleted (no existing consumers) - SwiftLint config enables and disables same rules
- Unused
colIdxvariable in A2UIComponentView - Duplicate test helpers across test files
ToolExecutionResultusesData?— typed result would be more ergonomic- Builder free functions pollute global namespace
- A2UI action routing sends
"[A2UI Action] {json}"as plain text — fragile A2UISurfaceViewdecodes JSON in viewbody@unchecked SendableonStringCapturein tests- Testing deprecated
AGUIToolsstruct - Unbounded SSE buffer growth — no max size guard
What's Good
Despite the volume of findings, there is a lot of quality work here:
- Clean dependency DAG — Core → Client/Tools → AgentSDK, no cycles, correct layering
- Value types everywhere — all events/messages are structs with proper
Sendable - Actor usage — ConversationHistoryManager, StateManager, DefaultToolRegistry correctly use actors
- Extensible decoder — registry pattern allows swapping individual event handlers
- Shared test protocols —
EventDecodingErrorTestseliminates duplication across 20+ event types elegantly - ChatApp quality — well-structured example with streaming, tools, A2UI, agent management
- Complete coverage of existing events — all 26 non-Reasoning AG-UI events implemented
Summary
The core architecture is sound and the Swift craftsmanship is evident. The main issues are:
- Protocol conformance — missing Reasoning events, field name mismatches, non-standard structures
- Correctness bugs — RawEvent reads wrong wire key, Bool/Int encode priority, SSE
\r\n, duplicate messages in history - Dead features — RetryPolicy, CircuitBreaker never wired up
- Test gaps — StatefulAgUiAgent, ToolExecutionManager, MessageEncoder untested
Recommended Actions
Must-Fix (blocking)
- Add all 7 Reasoning events +
ReasoningMessage+reasoningrole - Add
encryptedValue: String?to all messages andToolCall - Fix
RawEventDTOto read"event"instead of"data", addsource - Fix
RunErrorEventstructure (flatmessage/code, dropthreadId/runId/ErrorInfo) - Add missing fields:
RunStartedEvent.parentRunId/input,RunFinishedEvent.result,TextMessageStartEvent.name - Fix Bool/Int encode priority in
JSONCodingHelpers - Fix SSE parser to handle
\r\nline endings - Fix duplicate assistant message bug in
StatefulAgUiAgent - Add tests for
StatefulAgUiAgent,ToolExecutionManager,MessageEncoder - Remove
@frozenfromEventType
Should-Fix (strong recommendation)
- Consolidate config types (5 → 2)
- Either integrate or remove
RetryPolicy/CircuitBreaker/ToolErrorHandler - Fix
PatchApplicatorRFC 6901"/"path handling - Document iOS-only limitation and missing reconnection strategy
A note on scope: The severity labels indicate what blocks merge vs. what's a recommendation, but a robust SDK addresses everything on this list — not just the Critical and High items. The Medium, Low, and Nit findings represent the difference between an SDK that works and one that developers trust and enjoy using. We'd love to see this become the definitive Swift SDK for AG-UI.
Hi @jpr5, thanks for the detailed feedback — I really appreciate it. I based my implementation on the Kotlin SDK, which is why I missed some of the required changes, like the 7 reasoning event types. |
|
@jpr5 Are there documentation for demo/test server setup that I can leverage to run tests? |
- Replace 23-line MIT license blocks with single copyright line (292 files)
- Remove deprecated THINKING_* event types, DTOs, registry, and tests;
add transparent backward-compat decoder layer that remaps THINKING_*
wire events to REASONING_* (mirrors TypeScript BackwardCompatibility approach)
- Remove ThinkingTelemetryState and all thinking fields from AgentState,
AbstractAgent, and HttpAgent
- Delete dead AGUICore/AGUITools placeholder structs and their tests
- Fix SwiftLint large_tuple config (nested YAML form)
- Fix unused colIdx variable in A2UIComponentView
- Replace fragile "[A2UI Action] {json}" string prefix with typed
A2UIActionEnvelope: Encodable struct
- Move A2UISurfaceView JSON decode from body to init
- Add SseParser.maxBufferByteCount (10 MB) guard against unbounded growth
- Add ToolExecutionResult.decode<T: Decodable>(as:using:) convenience
- Wrap AgentBuilders free functions in public enum namespace
- Extract shared MockToolRegistry actor to Tests/Mocks/
- Add .claude/worktrees/ to .gitignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: paduh <paduh@users.noreply.github.com>
- Replace 23-line MIT license blocks with single copyright line (292 files)
- Remove deprecated THINKING_* event types, DTOs, registry, and tests;
add transparent backward-compat decoder layer that remaps THINKING_*
wire events to REASONING_* (mirrors TypeScript BackwardCompatibility approach)
- Remove ThinkingTelemetryState and all thinking fields from AgentState,
AbstractAgent, and HttpAgent
- Delete dead AGUICore/AGUITools placeholder structs and their tests
- Fix SwiftLint large_tuple config (nested YAML form)
- Fix unused colIdx variable in A2UIComponentView
- Replace fragile "[A2UI Action] {json}" string prefix with typed
A2UIActionEnvelope: Encodable struct
- Move A2UISurfaceView JSON decode from body to init
- Add SseParser.maxBufferByteCount (10 MB) guard against unbounded growth
- Add ToolExecutionResult.decode<T: Decodable>(as:using:) convenience
- Wrap AgentBuilders free functions in public enum namespace
- Extract shared MockToolRegistry actor to Tests/Mocks/
- Add .claude/worktrees/ to .gitignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
31635f3 to
e385b8b
Compare
jpr5
left a comment
There was a problem hiding this comment.
Review — Swift Community SDK
299 files, 47,943 additions. Reviewed across protocol fidelity, core types/decoding, client transport/SSE, tools/agent SDK, test quality, example app, and build config/repo hygiene.
Critical
1. RunFinishedOutcome is structurally incompatible with the AG-UI wire protocol.
Swift defines this as a flat string enum (COMPLETED, CANCELLED, MAX_ITERATIONS_REACHED), but the TypeScript spec sends a discriminated union object: { type: "success" } or { type: "interrupt", interrupts: [...] }. The DTO decoder tries RunFinishedOutcome(rawValue:) against a raw string, but the wire format is a JSON object — so the decoder always falls through to .completed. Interrupt outcomes are silently lost.
Sources/AGUICore/Events/LifeCycleEvents/RunFinishedOutcome.swiftSources/AGUICore/Decoding/EventDTO/LifeCycleEventsDTO/RunFinishedEventDTO.swift
2. Interrupt, ResumeEntry, and RunAgentInput.resume are completely missing.
TypeScript defines InterruptSchema (id, reason, message, toolCallId, responseSchema, expiresAt, metadata), ResumeEntrySchema (interruptId, status, payload), and RunAgentInput.resume. None exist in the Swift SDK. Human-in-the-loop flows cannot work.
3. convertFromSnakeCase key decoding strategy is wrong for the AG-UI wire protocol.
AGUIEventDecoder.swift line ~180 sets .convertFromSnakeCase on the default JSONDecoder. The AG-UI wire protocol uses camelCase keys (messageId, toolCallId, toolCallName, parentMessageId, etc.). With this strategy, the decoder expects message_id but the wire sends messageId. Roughly half the event DTOs use standard Decodable (not manual JSONSerialization) and will silently drop or fail on camelCase fields.
Major
4. Streaming buffer never yields during iteration.
Sources/AGUIClient/Streaming/AsyncSequence+Buffering.swift lines 43-78 — the buffered(limit:strategy:) method accumulates elements in a local array during for try await but never calls continuation.yield() inside the loop. Elements are only yielded after the upstream sequence completes. For a real-time SSE stream, the consumer receives zero events until the entire stream finishes.
5. SSE retry field is silently ignored.
Sources/AGUIClient/Streaming/SseParser.swift lines 163-170 — the parser handles data, id, and event but drops retry in the default branch. Per the WHATWG SSE spec, retry: instructs the client on reconnection interval. The value should at minimum be surfaced so the transport layer can use it.
6. JSON Pointer parsePath drops empty path segments.
Sources/AGUIClient/State/PatchApplicator.swift line 325 — path.dropFirst().split(separator: "/") uses the default omittingEmptySubsequences: true. Per RFC 6901, /a//b references key a, then empty-string key "", then key b. The current code parses it as ["a", "b"], dropping the empty segment. Fix: split(separator: "/", omittingEmptySubsequences: false).
7. URLSession byte bridge Task has no cancellation propagation.
Sources/AGUIClient/Transport/URLSessionHTTPClient.swift lines 101-113 — the Task bridging URLSession.AsyncBytes to AsyncThrowingStream<UInt8> does not set continuation.onTermination or check Task.isCancelled. If the consumer cancels, the network Task keeps running. Resource leak for long SSE streams.
8. Backward-compat THINKING→REASONING remapping is stateless — breaks message correlation.
Sources/AGUICore/Decoding/AGUIEventDecoder.swift lines 261-290 — each remapped event gets a fresh UUID().uuidString for messageId. The TypeScript backward-compat middleware is stateful: it stores currentReasoningId and currentMessageId across events in a run, so start/content/end events share the same messageId. The Swift version breaks any consumer logic that correlates reasoning events by messageId.
9. StatefulAgUiAgent ignores bearerToken and apiKey.
Sources/AGUIAgentSDK/StatefulAgUiAgent.swift line 113 — passes configuration.headers directly to HttpAgentConfiguration instead of calling configuration.buildHeaders(). If a user sets config.bearerToken or config.apiKey, those values are silently ignored. AgUiAgent correctly uses buildHeaders().
10. ToolExecutionError.executionFailed holds non-Sendable Error.
Sources/AGUITools/Core/ToolExecutor.swift line 248 — the enum is declared Sendable but case executionFailed(toolName: String, underlyingError: Error) stores a bare Error unconstrained to Sendable. This is a compile error under Swift 6 strict concurrency.
11. ToolExecutor.validate() is never called.
Sources/AGUITools/Core/ToolExecutor.swift line 89 — validate(toolCall:) is defined on the protocol with a default implementation, but neither DefaultToolRegistry.execute() nor ToolExecutionManager.executeToolCall() invokes it before executing. The validation pipeline is dead code.
12. README badge URLs point to contributor's fork.
Lines 3-4 link to https://github.com/paduh/ag-ui-swift/actions/workflows/ci.yml and docs.yml. These won't work from ag-ui-protocol/ag-ui. README also references non-existent scripts (test-docs-local.sh, verify-docs-structure.sh, scripts/install-git-hooks.sh) and non-existent docs/ directory. CONTRIBUTING.md line 119 has a your-username placeholder URL.
13. LICENSE year is 2024, should be 2025. Copyright holder "AGUISwift Contributors" is inconsistent with repo root license.
Minor
14. SSE parser doesn't reject id fields containing U+0000.
Per WHATWG SSE spec, if the id field value contains a null character, the field must be ignored. SseParser.swift line 168 unconditionally sets id = value.
15. SSE buffer overflow guard silently drops data.
SseParser.swift lines 106-109 — when buffer exceeds 10MB, it resets and returns empty. No error, no logging. Callers have no way to know data was lost.
16. HttpTransport creates a URLSession per init, never invalidated.
Sources/AGUIClient/Transport/HttpTransport.swift lines 61-89 — URLSessions hold OS-level socket pools. Without invalidateAndCancel(), these sessions leak across multiple HttpAgent instantiations.
17. HttpAgent creates duplicate transports.
Sources/AGUIClient/HttpAgent.swift lines 6-38 — all three initializers create both an HttpAgentTransport and a separate HttpTransport, each with their own URLSession. The httpTransport field appears to exist solely for run(_ input:endpoint:), duplicating functionality already in abstractAgent.
18. Header values not validated for CRLF injection.
Sources/AGUIClient/Transport/HttpAgentConfiguration.swift lines 58-70 — buildHeaders() passes bearer token, API key, and user-provided header values directly without sanitizing for \r\n. URLSession on Apple platforms rejects these, but custom HTTPClient implementations might not.
19. SystemMessage.content and ToolMessage.content are optional in Swift but required in TypeScript.
TypeScript SystemMessageSchema and ToolMessageSchema both have content: z.string() (required). Swift allows nil. A Swift-produced message with nil content would fail TypeScript validation.
20. MessagesSnapshotEvent.messages stored as raw Data instead of typed [Message].
TypeScript has messages: z.array(MessageSchema). Swift stores opaque bytes — no structural validation at decode time.
21. UserMessage Equatable ignores contentParts content.
Sources/AGUICore/Types/Messages/UserMessage.swift lines 157-163 — == only checks isMultimodal (a bool), not the actual content parts array. Two messages with different parts but same metadata compare as equal. Same issue in hashValue.
22. encryptedValue dropped from message encoding for all message types.
Sources/AGUICore/Encoding/MessageEncoder.swift — encodeDeveloperMessage, encodeSystemMessage, encodeUserMessage, encodeAssistantMessage all omit encryptedValue. Round-trip data loss.
23. AnyCodable tries Int before Bool.
Sources/AGUICore/Types/Tools/Tool.swift lines 210-232 — Swift's Codable can decode JSON true/false as Int (1/0). Check Bool before Int to prevent silent type coercion.
24. AgUiAgent placeholder httpAgent for transport-based init.
Sources/AGUIAgentSDK/AgUiAgent.swift lines 56-57 — creates a dummy HttpAgent(baseURL: "https://placeholder.local"). If a user calls subscribe() on a transport-constructed agent, they get a subscription to nothing.
25. sendMessage generates a new threadId per call by default.
Sources/AGUIAgentSDK/AgUiAgent.swift line 71 — default threadId: String = UUID().uuidString means every call gets a different thread, making multi-turn conversation impossible unless the caller explicitly passes one.
26. try? silently swallows sendToolResponse errors.
Sources/AGUITools/Core/ToolExecutionManager.swift lines 185, 203, 213 — if the response handler fails to deliver a tool result, the failure is silently ignored. The agent stalls waiting for a result that was never delivered.
27. Shared CircuitBreaker across all tools.
Sources/AGUITools/Core/ToolExecutionManager.swift line 40 — single ToolErrorHandler for all tools. One consistently-failing tool opens the breaker for all tools.
28. HttpTransportTests — 8 of 15 tests assert nothing.
Tests like testTransportInitialization, testTransportWithCustomTimeout, testTransportWithCustomHeaders, testTransportIsActor just create an object inside withCheckedContinuation and resume. testURLErrorTimeoutMapping and testURLErrorCancelledMapping are empty methods. These inflate test count.
29. No tests for ToolErrorHandler, CircuitBreaker, ToolExecutionManager, ClientToolResponseHandler.
~650 LOC of untested production code covering the entire tool execution reliability layer.
30. Platform version inconsistency.
Package.swift says .iOS(.v16). CLAUDE.md says "iOS 15+". One is wrong.
31. CLAUDE.md references non-existent paths and phantom features.
References Sources/SwiftAgents/Core/Protocols/ (doesn't exist — actual path is Sources/AGUICore/). Documents ConversationMemory, VectorMemory, SummaryMemory which don't exist in the source tree.
32. ARCHITECTURE.md lists tvOS and watchOS.
Package.swift only declares iOS and macOS platforms.
33. Deprecated onChange(of:) single-parameter form used in 4 places in ChatApp.
AgentFormView.swift:41, ChatView.swift:77, A2UIComponentView.swift:233, A2UIComponentView.swift:262 — produces deprecation warnings on Xcode 15+.
34. Fully-stubbed ClawgUI feature in ChatApp.
3 source files + views + tests for an unimplemented feature. LiveClawgUIPairingHTTPClient is all TODOs. Shipping a non-functional feature in a showcase app is confusing — either remove it or implement it.
35. ClawgUIDetector uses .regularExpression for a literal string match.
ClawgUIDetector.swift:17-19 — "/v1/clawg-ui" is a literal, not a regex. Use .contains().
36. NSAllowsArbitraryLoads = true in ChatApp Info.plist.
ATS completely disabled. Fine for a demo but should be documented.
37. chatRows computed property sorts on every SwiftUI evaluation.
ChatUIState.swift:62-66 — merges and sorts two arrays on every access from ChatView.body.
38. ChatApp accessibility is minimal.
One accessibilityLabel in the entire app. Send button, cancel button, agent list rows, ephemeral banners, typing indicators — all missing labels/hints.
39. Package.resolved committed for an example app.
.gitignore has Package.resolved commented out. For a library it's standard, but the ChatApp is an application — pinned transitive deps will go stale.
40. ToolMessage.content DTO maps nil to empty string.
ToolMessageDTO.swift line 39: content ?? "". Callers can't distinguish "tool returned empty" from "tool returned no content".
41. ConversationHistoryManager.trim doc says maxLength excludes system message, but code counts it against the limit.
Sources/AGUIAgentSDK/ConversationHistoryManager.swift line 75 — if maxLength is 5, you get 1 system + 4 non-system = 5, not 1 + 5 = 6 as the doc implies.
42. Duplicated buildHeaders() logic.
AgUiAgentConfig.swift and StatefulAgUiAgentConfig.swift have identical copy-pasted buildHeaders() methods. Extract to shared utility.
43. Default validate() implementation missing nonisolated keyword.
Sources/AGUITools/Core/ToolExecutor.swift line 107 — protocol requires nonisolated func validate(toolCall:) but the default extension implementation omits it. Same for maximumExecutionTime() at line 114.
44. CI workflow uses unpinned actions/checkout@v4.
Supply-chain risk — should pin to a full commit SHA.
45. CI workflow display name is just unit.
Same name as the Kotlin SDK's workflow. Use something distinguishable like unit-swift in the Actions tab.
46. TextStyle.fontSize decoded but never applied.
A2UIComponent.swift:11 declares fontSize, but A2UIComponentView.swift:139-146 resolvedFont only handles fontWeight.
47. testStreamHandlesUnknownEventType has weak assertions.
EventStreamTests.swift lines 156-189 — uses XCTAssertGreaterThanOrEqual with branching logic instead of asserting exact expected behavior.
48. testParserIsNotThreadSafe is a no-op.
SseParserTests.swift lines 437-445 — creates a parser, parses data, then XCTAssertTrue(true).
49. ChatAppStoreTests wall-clock sleep.
test_toolCallEnd_schedulesEphemeralDismissal depends on Task.sleep(for: .seconds(1.2)) — flaky on slow CI.
50. .gitignore includes sections for unused tools (CocoaPods, Carthage, fastlane) and local dev artifacts (MEDIUM_ARTICLE.md, docs-test/). Template noise.
…-ui into feature/add-swift-community-sdk
Three correctness issues identified in PR review:
1. RunFinishedOutcome: replace flat string enum with a discriminated
union matching the wire format. The wire sends a JSON object
({ "type": "success" } or { "type": "interrupt", "interrupts": [...] }),
not a string. The old decoder always fell back to .completed, silently
losing interrupt outcomes. RunFinishedEvent.outcome is now Optional;
nil means absent/null (legacy producer compat).
2. Human-in-the-loop types: add Interrupt and ResumeEntry, and add
RunAgentInput.resume: [ResumeEntry]?. These are required for HITL
flows. resume is encoded as absent (not null) when nil.
3. Key decoding strategy: remove convertFromSnakeCase from the default
JSONDecoder. The AG-UI wire protocol uses camelCase throughout; the
strategy was semantically wrong and a latent risk for edge cases.
Also fix ResumeEntry payload handling to correctly encode/decode all
JSON value types (object, array, bool, number, string), not just dicts.
1414 tests, 0 failures.
- buffered(): rewrite to yield immediately with bufferingPolicy instead of accumulating, fixing SSE streams that never delivered events - THINKING→REASONING remap: add stateful ThinkingRemapState to correlate IDs across event sequences (mirrors TS BackwardCompatibility_0_0_45) - URLSessionHTTPClient: wire onTermination to cancel producer Task on stream cancellation - SSE parser: parse retry field per WHATWG spec - PatchApplicator: preserve empty segments in RFC 6901 JSON Pointer paths - ToolExecutor: fix Sendable violation in executionFailed associated value - ToolRegistry: validate before execute; throw validationFailed on failure - StatefulAgUiAgent: call buildHeaders() to include auth in requests - Fix repo URLs in CONTRIBUTING.md, README.md, and LICENSE - Add tests for all fixes
Issues 14-24 (previous session): - Fix SseParser.parse() to throw on malformed input - Remove duplicate URLSession creation in HttpAgent; route through HttpAgentTransport - Replace AgUiAgent placeholder HttpAgent with two-optionals pattern (httpAgent/abstractAgent) - Change MessagesSnapshotEvent.messages from Data to [any Message]; remove parsedMessages() - Update MessagesSnapshotEventDTO to decode typed message array via MessageDecoder - Make SystemMessage.content non-optional (default ""); ToolMessage.content non-optional - Add MessageEncoder with per-role handler registry and unsupportedRole/invalidMessageType errors - Fix SseParserTests, SystemMessageTests, ToolMessageTests, MessageEncoderTests, MessagesSnapshotEventTests for breaking API changes Issues 25-27 (this session): - Fix sendMessage threadId stability: generate defaultThreadId once at init, not per call; change threadId param from String to String?; resolve via threadId ?? defaultThreadId - Fix try? silencing on all three executeToolCall delivery paths: success, .fail, .circuitOpen now use do/catch; delivery failures surface in .failed event error string rather than being dropped silently (agent stall risk) - Fix shared circuit breaker cross-contamination: replace single ToolErrorHandler with per-tool errorHandlers dictionary keyed by tool name; init now takes errorHandlerConfig instead of errorHandler; each tool gets an independent circuit breaker 1437 tests, 0 failures
- fix: trim(maxLength:) now excludes system message from count (issue 41) - fix: ToolExecutor extension defaults gain nonisolated keyword (issue 43) - fix: resolvedFont applies fontSize when non-nil (issue 46) - fix: remove NSAllowsArbitraryLoads; NSAllowsLocalNetworking sufficient (issue 36) - refactor: extract AgentHeaderBuilder to eliminate duplicate buildHeaders (issue 42) - test: replace branching unknown-event test with exact assertions (issue 47) - test: add tolerant-decoder companion test for UnknownEvent (issue 47) - test: replace no-op testParserIsNotThreadSafe (issue 48) - test: inject ephemeralDismissDelayOverrides to avoid 1s sleep in tests (issue 49) - ci: rename workflow to unit-swift-sdk; pin checkout action SHA (issues 44, 45) - feat(a11y): add accessibilityLabel/Hint to send/cancel buttons, agent rows, ephemeral banners, and typing indicator (issue 38) - docs: add toDomain() doc comment to ToolMessageDTO (issue 40) - chore: remove stale CocoaPods/Carthage/fastlane sections from .gitignore (issue 50)
Replace the broad catch-all in EventStream.AsyncIterator.next() with per-category error handling that matches the TypeScript reference. invalidJSON is skipped — malformed bytes are not a protocol violation and the stream can recover. All other EventDecodingError cases (unknownEventType, missingTypeField, decodingFailed, unsupportedEventType) are re-thrown, terminating the stream. This aligns with http.ts which calls eventSubject.error(err) for all decode failures. Also fix testStreamHandlesToolCallScenario: corrects the TOOL_CALL_START wire field from "toolName" to "toolCallName" (per ToolCallStartEvent CodingKeys) and replaces the XCTAssertGreaterThanOrEqual(count, 3) weak guard with exact assertions on all 5 events.
Summary
sdks/community/swift/) implementing the AG-UI protocol for Apple platforms (iOS 15+, macOS 13+)async/await, actors, and@MainActorfor safe concurrencySDK Architecture
Features
Test plan
Package.swiftbuilds cleanly (swift build)swift test) — 100+ test cases across all modulesNotes
🤖 Generated with Claude Code