From c5977fffaf7ff27cad39b4ec71b2216fde764361 Mon Sep 17 00:00:00 2001 From: paduh Date: Sat, 2 May 2026 17:43:49 -0400 Subject: [PATCH 1/2] fic for pr comments about comments --- .../Sources/Models/SupplementalMessage.swift | 1 - Sources/AGUIAgentSDK/AgUiAgentConfig.swift | 17 --- Sources/AGUIAgentSDK/AgentBuilders.swift | 21 ---- Sources/AGUIAgentSDK/AgentMessage.swift | 16 --- Sources/AGUIAgentSDK/AgentViewModel.swift | 48 -------- .../AGUIAgentSDK/AgentViewModelCompat.swift | 46 ------- Sources/AGUIAgentSDK/ChatAgent.swift | 20 ---- .../ConversationHistoryManager.swift | 22 ---- Sources/AGUIAgentSDK/StatefulAgUiAgent.swift | 49 -------- .../StatefulAgUiAgentConfig.swift | 11 -- .../Tools/ClientToolResponseHandler.swift | 19 --- Sources/AGUIClient/AGUIClient.swift | 10 -- .../AGUIClient/State/DefaultApplyEvents.swift | 20 ---- .../AGUIClient/State/PatchApplicator.swift | 34 ------ Sources/AGUIClient/State/StateManager.swift | 30 ----- .../Streaming/AsyncSequence+Buffering.swift | 20 ---- .../Streaming/BufferingStrategy.swift | 5 - .../Streaming/ChunkTransformer.swift | 17 --- .../AGUIClient/Streaming/EventStream.swift | 38 ------ Sources/AGUIClient/Streaming/SseEvent.swift | 23 ---- Sources/AGUIClient/Streaming/SseParser.swift | 43 ------- .../Subscriber/AgentSubscriber.swift | 15 --- Sources/AGUIClient/Transport/HTTPClient.swift | 19 --- .../AGUIClient/Transport/HttpTransport.swift | 17 --- .../Transport/URLSessionHTTPClient.swift | 16 --- Sources/AGUICore/AGUICore.swift | 1 - .../AGUICore/Decoding/AGUIEventDecoder.swift | 113 ------------------ .../AGUICore/Decoding/MessageDecoder.swift | 52 -------- .../AGUICore/Encoding/MessageEncoder.swift | 44 ------- Sources/AGUICore/EventType.swift | 2 +- Sources/AGUICore/Events/AGUIEvent.swift | 1 - .../ActivityEvents/ActivityDeltaEvent.swift | 2 - .../ActivitySnapshotEvent.swift | 1 - .../LifeCycleEvents/RunErrorEvent.swift | 1 - .../LifeCycleEvents/RunFinishedEvent.swift | 1 - .../LifeCycleEvents/RunStartedEvent.swift | 1 - .../ReasoningEvents/ReasoningEndEvent.swift | 4 +- .../ReasoningMessageChunkEvent.swift | 1 - .../ReasoningMessageContentEvent.swift | 3 +- .../ReasoningMessageEndEvent.swift | 1 - .../ReasoningMessageStartEvent.swift | 3 +- .../ReasoningEvents/ReasoningStartEvent.swift | 3 +- .../Events/SpecialEvents/CustomEvent.swift | 11 +- .../Events/SpecialEvents/RawEvent.swift | 1 - .../StateEvents/MessagesSnapshotEvent.swift | 1 - .../StateEvents/StateSnapshotEvent.swift | 1 - .../TextMessageChunkEvent.swift | 1 - .../TextMessageContentEvent.swift | 1 - .../TextMessageEndEvent.swift | 1 - .../TextMessageStartEvent.swift | 1 - .../ToolCallEvents/ToolCallArgsEvent.swift | 1 - .../ToolCallEvents/ToolCallChunkEvent.swift | 1 - .../ToolCallEvents/ToolCallEndEvent.swift | 1 - .../ToolCallEvents/ToolCallResultEvent.swift | 1 - .../ToolCallEvents/ToolCallStartEvent.swift | 1 - Sources/AGUICore/Events/UnknownEvent.swift | 43 ------- .../Types/AgentExecution/Context.swift | 36 ------ .../Types/AgentExecution/RunAgentInput.swift | 45 ------- .../AgentExecution/RunAgentInputBuilder.swift | 50 -------- .../AGUICore/Types/AgentExecution/State.swift | 52 -------- .../InputContent/AudioInputContent.swift | 1 - .../InputContent/BinaryInputContent.swift | 39 ------ .../InputContent/DocumentInputContent.swift | 1 - .../InputContent/ImageInputContent.swift | 1 - .../Types/InputContent/InputContent.swift | 22 ---- .../Types/InputContent/TextInputContent.swift | 30 ----- .../InputContent/VideoInputContent.swift | 1 - .../Types/Messages/ActivityMessage.swift | 62 ---------- .../Types/Messages/AssistantMessage.swift | 45 ------- .../Types/Messages/DeveloperMessage.swift | 34 ------ Sources/AGUICore/Types/Messages/Message.swift | 19 --- .../Types/Messages/ReasoningMessage.swift | 17 --- Sources/AGUICore/Types/Messages/Role.swift | 1 - .../Types/Messages/SystemMessage.swift | 35 ------ .../AGUICore/Types/Messages/ToolMessage.swift | 36 ------ .../AGUICore/Types/Messages/UserMessage.swift | 39 ------ .../AGUICore/Types/Tools/FunctionCall.swift | 37 ------ Sources/AGUICore/Types/Tools/Tool.swift | 73 ----------- Sources/AGUICore/Types/Tools/ToolCall.swift | 44 ------- .../Utilities/JSONCodingHelpers.swift | 9 -- .../AGUITools/Core/ToolExecutionContext.swift | 18 --- .../AGUITools/Core/ToolExecutionManager.swift | 19 --- .../AGUITools/Core/ToolExecutionResult.swift | 16 --- Sources/AGUITools/Core/ToolExecutor.swift | 71 ----------- .../Registry/ToolExecutionStats.swift | 15 --- Sources/AGUITools/Registry/ToolRegistry.swift | 36 ------ .../Transport/MockHTTPClient.swift | 21 ---- .../Helpers/AGUIEventDecoderTestHelpers.swift | 13 -- .../Helpers/EventDecodingErrorTests.swift | 18 --- 89 files changed, 6 insertions(+), 1827 deletions(-) delete mode 100644 Sources/AGUICore/AGUICore.swift diff --git a/Examples/ChatApp/Sources/Models/SupplementalMessage.swift b/Examples/ChatApp/Sources/Models/SupplementalMessage.swift index 9980842..2ba885d 100644 --- a/Examples/ChatApp/Sources/Models/SupplementalMessage.swift +++ b/Examples/ChatApp/Sources/Models/SupplementalMessage.swift @@ -4,7 +4,6 @@ import Foundation /// A non-agent system message injected into the chat list for lifecycle events. /// -/// Examples: agent connection confirmation, inline error notifications. /// Rendered as distinct, non-interactive rows by `SupplementalMessageBubbleView`. struct SupplementalMessage: Identifiable, Sendable { let id: String diff --git a/Sources/AGUIAgentSDK/AgUiAgentConfig.swift b/Sources/AGUIAgentSDK/AgUiAgentConfig.swift index 1508b04..22fbc13 100644 --- a/Sources/AGUIAgentSDK/AgUiAgentConfig.swift +++ b/Sources/AGUIAgentSDK/AgUiAgentConfig.swift @@ -10,23 +10,6 @@ import Foundation /// `AgUiAgentConfig` provides all options for a stateless AG-UI agent, including /// authentication helpers, tool registry, per-request context, and timeout tuning. /// -/// ## Example -/// -/// ```swift -/// let agent = AgUiAgent(url: agentURL) { config in -/// config.bearerToken = "sk-…" -/// config.systemPrompt = "You are a helpful assistant." -/// config.toolRegistry = myRegistry -/// } -/// ``` -/// -/// ## Auth Convenience -/// -/// Setting `bearerToken` or `apiKey` automatically merges the corresponding header -/// into the final header dictionary via ``buildHeaders()``. Explicit entries in -/// ``headers`` take precedence over auto-generated auth headers. -/// -/// - SeeAlso: ``AgUiAgent``, ``AgentBuilders`` public struct AgUiAgentConfig: Sendable { // MARK: - Auth diff --git a/Sources/AGUIAgentSDK/AgentBuilders.swift b/Sources/AGUIAgentSDK/AgentBuilders.swift index e1d77a3..c78dd76 100644 --- a/Sources/AGUIAgentSDK/AgentBuilders.swift +++ b/Sources/AGUIAgentSDK/AgentBuilders.swift @@ -9,27 +9,6 @@ import Foundation /// `AgentBuilders` mirrors the `AgentBuilders` pattern from the Kotlin SDK, /// providing clean one-liner factory calls for the most common agent configurations. /// -/// ## Examples -/// -/// ```swift -/// // Bearer-token authenticated agent -/// let agent = AgentBuilders.agentWithBearer(url: agentURL, token: "sk-…") -/// -/// // API-key authenticated agent -/// let agent = AgentBuilders.agentWithApiKey(url: agentURL, apiKey: "my-key") -/// -/// // Agent with custom tool registry -/// let agent = AgentBuilders.agentWithTools(url: agentURL, registry: myRegistry) -/// -/// // Stateful chat agent with a system prompt -/// let agent = AgentBuilders.chatAgent(url: agentURL, systemPrompt: "You are a helpful assistant.") -/// -/// // Stateful agent with pre-seeded JSON state -/// let agent = AgentBuilders.statefulAgent(url: agentURL, initialState: Data("{\"mode\":\"creative\"}".utf8)) -/// -/// // Debug agent that logs verbose pipeline output -/// let agent = AgentBuilders.debugAgent(url: agentURL) -/// ``` public enum AgentBuilders { // MARK: - Stateless agents diff --git a/Sources/AGUIAgentSDK/AgentMessage.swift b/Sources/AGUIAgentSDK/AgentMessage.swift index d9c7152..67beed7 100644 --- a/Sources/AGUIAgentSDK/AgentMessage.swift +++ b/Sources/AGUIAgentSDK/AgentMessage.swift @@ -9,22 +9,6 @@ import Foundation /// `AgentMessage` is a concrete value type that is `Identifiable` and `Equatable`, /// making it safe to use directly in `List`, `ForEach`, and `@Observable` properties. /// -/// ## Usage in SwiftUI -/// -/// ```swift -/// List(viewModel.messages) { message in -/// MessageBubble(message: message) -/// } -/// ``` -/// -/// ## Relationship to the protocol layer -/// -/// `AgentMessage` is built by ``AgentViewModel`` / ``AgentViewModelCompat`` as events -/// arrive from the stream. It is not decoded from the wire — it is assembled in the -/// view model from `TextMessageStartEvent`, `TextMessageContentEvent`, and -/// `TextMessageEndEvent` events. -/// -/// - SeeAlso: ``AgentViewModel``, ``AgentViewModelCompat``, ``AgentError`` public struct AgentMessage: Sendable, Identifiable, Equatable { // MARK: - Role diff --git a/Sources/AGUIAgentSDK/AgentViewModel.swift b/Sources/AGUIAgentSDK/AgentViewModel.swift index 78861a3..841c470 100644 --- a/Sources/AGUIAgentSDK/AgentViewModel.swift +++ b/Sources/AGUIAgentSDK/AgentViewModel.swift @@ -16,54 +16,6 @@ import Observation /// SwiftUI views that read `messages`, `isRunning`, or `lastError` are re-rendered only /// when those specific properties change. /// -/// For iOS 16 / macOS 13 support use ``AgentViewModelCompat`` (`ObservableObject`). -/// -/// ## Basic SwiftUI usage -/// -/// ```swift -/// @State private var vm = AgentViewModel( -/// agent: StatefulAgUiAgent(baseURL: agentURL) -/// ) -/// -/// var body: some View { -/// VStack { -/// ScrollView { -/// ForEach(vm.messages) { message in -/// MessageBubble(message: message) -/// } -/// } -/// HStack { -/// TextField("Message", text: $draft) -/// Button("Send") { -/// Task { await vm.send(draft) } -/// } -/// .disabled(vm.isRunning) -/// } -/// if let error = vm.lastError { -/// Text(error.localizedDescription).foregroundColor(.red) -/// } -/// } -/// } -/// ``` -/// -/// ## Streaming text -/// -/// As the agent streams tokens, each `TextMessageContentEvent` delta is appended -/// to the last assistant `AgentMessage` in place — the message `id` stays the -/// same so SwiftUI animates the existing row rather than replacing it. -/// -/// ## Error handling -/// -/// - `RunErrorEvent` from the agent sets `lastError` as ``AgentError/runError(message:code:)`` -/// - Transport-level failures (network, timeout) set `lastError` as whatever `Error` was thrown -/// - `lastError` is cleared automatically at the start of each new `send()` call -/// -/// ## Thread safety -/// -/// `AgentViewModel` is isolated to `@MainActor`. Call `send()` and `clear()` -/// from SwiftUI button handlers or `Task { }` blocks — they are already on the main actor. -/// -/// - SeeAlso: ``AgentViewModelCompat``, ``ChatAgent``, ``AgentMessage``, ``AgentError`` @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Observable @MainActor diff --git a/Sources/AGUIAgentSDK/AgentViewModelCompat.swift b/Sources/AGUIAgentSDK/AgentViewModelCompat.swift index f2fe3b0..497cf19 100644 --- a/Sources/AGUIAgentSDK/AgentViewModelCompat.swift +++ b/Sources/AGUIAgentSDK/AgentViewModelCompat.swift @@ -11,52 +11,6 @@ import Foundation /// macOS 14+, prefer the zero-boilerplate ``AgentViewModel`` which uses the /// `@Observable` macro instead. /// -/// ## Basic SwiftUI usage -/// -/// ```swift -/// @StateObject private var vm = AgentViewModelCompat( -/// agent: StatefulAgUiAgent(baseURL: agentURL) -/// ) -/// -/// var body: some View { -/// VStack { -/// ScrollView { -/// ForEach(vm.messages) { message in -/// MessageBubble(message: message) -/// } -/// } -/// HStack { -/// TextField("Message", text: $draft) -/// Button("Send") { -/// Task { await vm.send(draft) } -/// } -/// .disabled(vm.isRunning) -/// } -/// if let error = vm.lastError { -/// Text(error.localizedDescription).foregroundColor(.red) -/// } -/// } -/// } -/// ``` -/// -/// ## Streaming text -/// -/// As the agent streams tokens, each `TextMessageContentEvent` delta is appended -/// to the last assistant `AgentMessage` in place — the message `id` stays the -/// same so SwiftUI animates the existing row rather than replacing it. -/// -/// ## Error handling -/// -/// - `RunErrorEvent` from the agent sets `lastError` as ``AgentError/runError(message:code:)`` -/// - Transport-level failures (network, timeout) set `lastError` as whatever `Error` was thrown -/// - `lastError` is cleared automatically at the start of each new `send()` call -/// -/// ## Thread safety -/// -/// `AgentViewModelCompat` is isolated to `@MainActor`. Call `send()` and `clear()` -/// from SwiftUI button handlers or `Task { }` blocks — they are already on the main actor. -/// -/// - SeeAlso: ``AgentViewModel``, ``ChatAgent``, ``AgentMessage``, ``AgentError`` @MainActor public final class AgentViewModelCompat: ObservableObject { diff --git a/Sources/AGUIAgentSDK/ChatAgent.swift b/Sources/AGUIAgentSDK/ChatAgent.swift index 77d7e2d..6ebd24b 100644 --- a/Sources/AGUIAgentSDK/ChatAgent.swift +++ b/Sources/AGUIAgentSDK/ChatAgent.swift @@ -10,26 +10,6 @@ import Foundation /// agent implementation and makes both the view models and any custom agent /// implementations fully testable through mocks. /// -/// ## Built-in conformances -/// -/// ``StatefulAgUiAgent`` conforms to `ChatAgent` out of the box via a retroactive -/// extension in this file. -/// -/// ## Custom agents -/// -/// ```swift -/// struct MockChatAgent: ChatAgent { -/// func chat(message: String, threadId: String) async throws -/// -> AsyncThrowingStream -/// { -/// // Return test events -/// } -/// -/// func clearHistory(threadId: String?) async {} -/// } -/// ``` -/// -/// - SeeAlso: ``AgentViewModel``, ``AgentViewModelCompat``, ``StatefulAgUiAgent`` public protocol ChatAgent: Sendable { /// Sends a user message and returns the resulting AG-UI event stream. diff --git a/Sources/AGUIAgentSDK/ConversationHistoryManager.swift b/Sources/AGUIAgentSDK/ConversationHistoryManager.swift index 6313849..43bfade 100644 --- a/Sources/AGUIAgentSDK/ConversationHistoryManager.swift +++ b/Sources/AGUIAgentSDK/ConversationHistoryManager.swift @@ -18,28 +18,6 @@ import Foundation /// When trimming, the manager preserves system messages while removing the /// oldest user/assistant message pairs to fit within the specified limit. /// -/// ## Example -/// -/// ```swift -/// let manager = ConversationHistoryManager() -/// -/// // Add messages to a thread -/// await manager.append( -/// message: SystemMessage(id: "sys1", content: "You are helpful"), -/// to: "chat-1" -/// ) -/// await manager.append( -/// message: UserMessage(id: "usr1", content: "Hello"), -/// to: "chat-1" -/// ) -/// -/// // Get history -/// let history = await manager.history(for: "chat-1") -/// print(history.count) // 2 -/// -/// // Trim to size -/// await manager.trim(threadId: "chat-1", maxLength: 10) -/// ``` actor ConversationHistoryManager { /// Storage for per-thread conversation histories. private var threadHistories: [String: [any Message]] = [:] diff --git a/Sources/AGUIAgentSDK/StatefulAgUiAgent.swift b/Sources/AGUIAgentSDK/StatefulAgUiAgent.swift index bbd2f05..dcc6b90 100644 --- a/Sources/AGUIAgentSDK/StatefulAgUiAgent.swift +++ b/Sources/AGUIAgentSDK/StatefulAgUiAgent.swift @@ -11,55 +11,6 @@ import Foundation /// conversational AI interfaces. It automatically tracks message history per thread, /// manages state updates, and provides convenient methods for common patterns. /// -/// ## Basic Usage -/// -/// ```swift -/// let agent = StatefulAgUiAgent(baseURL: URL(string: "https://agent.example.com")!) -/// -/// let stream = try await agent.chat(message: "Hello!") -/// for try await event in stream { -/// if let content = event as? TextMessageContentEvent { -/// print(content.delta, terminator: "") -/// } -/// } -/// ``` -/// -/// ## Advanced Configuration -/// -/// ```swift -/// var config = StatefulAgUiAgentConfig(baseURL: agentURL) -/// config.systemPrompt = "You are a helpful AI assistant." -/// config.maxHistoryLength = 50 -/// config.timeout = .seconds(60) -/// -/// let agent = StatefulAgUiAgent(configuration: config) -/// -/// // Multi-turn conversation -/// _ = try await agent.chat(message: "What's the weather?") -/// _ = try await agent.chat(message: "And tomorrow?") // Maintains context -/// ``` -/// -/// ## Thread Management -/// -/// Each conversation can have its own thread with independent history: -/// -/// ```swift -/// // Conversation 1 -/// let stream1 = try await agent.chat(message: "Hello", threadId: "user-123") -/// -/// // Conversation 2 (separate history) -/// let stream2 = try await agent.chat(message: "Hi", threadId: "user-456") -/// ``` -/// -/// ## Features -/// -/// - **Automatic History**: User and assistant messages are tracked automatically -/// - **System Prompts**: Configurable system message for agent behavior -/// - **History Trimming**: Keeps conversations within token limits -/// - **State Management**: Tracks and updates agent state from events -/// - **Thread Safety**: Actor-based concurrency for safe multi-threaded use -/// -/// - SeeAlso: ``StatefulAgUiAgentConfig``, ``ConversationHistoryManager`` public final class StatefulAgUiAgent: Sendable { /// The underlying HTTP agent for communication. private let httpAgent: HttpAgent diff --git a/Sources/AGUIAgentSDK/StatefulAgUiAgentConfig.swift b/Sources/AGUIAgentSDK/StatefulAgUiAgentConfig.swift index fcfe7c4..e69e964 100644 --- a/Sources/AGUIAgentSDK/StatefulAgUiAgentConfig.swift +++ b/Sources/AGUIAgentSDK/StatefulAgUiAgentConfig.swift @@ -10,17 +10,6 @@ import Foundation /// This struct provides all configuration options for creating a stateful agent, /// including HTTP settings, conversation management, and agent behavior. /// -/// ## Example -/// -/// ```swift -/// var config = StatefulAgUiAgentConfig(baseURL: agentURL) -/// config.systemPrompt = "You are a helpful AI assistant." -/// config.maxHistoryLength = 50 -/// config.timeout = .seconds(60) -/// config.headers = ["Authorization": "Bearer token"] -/// -/// let agent = StatefulAgUiAgent(configuration: config) -/// ``` public struct StatefulAgUiAgentConfig: Sendable { /// The base URL of the AG-UI agent server. public var baseURL: URL diff --git a/Sources/AGUIAgentSDK/Tools/ClientToolResponseHandler.swift b/Sources/AGUIAgentSDK/Tools/ClientToolResponseHandler.swift index c970599..8096c7a 100644 --- a/Sources/AGUIAgentSDK/Tools/ClientToolResponseHandler.swift +++ b/Sources/AGUIAgentSDK/Tools/ClientToolResponseHandler.swift @@ -10,25 +10,6 @@ import Foundation /// Sends tool results back to the agent by initiating a new run containing /// only the tool result message. This mirrors the Kotlin `ClientToolResponseHandler`. /// -/// ## How it works -/// -/// When a tool call completes, the result must be delivered back to the agent -/// so it can continue the conversation. `ClientToolResponseHandler` does this -/// by constructing a minimal `RunAgentInput` containing the `ToolMessage` and -/// executing a new run through the same `HttpAgent`. The resulting events are -/// consumed and discarded — callers receive the results through the ongoing -/// conversation stream, not here. -/// -/// ## Example -/// -/// ```swift -/// let httpAgent = HttpAgent(baseURL: agentURL) -/// let handler = ClientToolResponseHandler(httpAgent: httpAgent) -/// let manager = ToolExecutionManager( -/// toolRegistry: registry, -/// responseHandler: handler -/// ) -/// ``` public final class ClientToolResponseHandler: ToolResponseHandler, Sendable { private let httpAgent: HttpAgent diff --git a/Sources/AGUIClient/AGUIClient.swift b/Sources/AGUIClient/AGUIClient.swift index 45d1368..ecd3c47 100644 --- a/Sources/AGUIClient/AGUIClient.swift +++ b/Sources/AGUIClient/AGUIClient.swift @@ -11,16 +11,6 @@ import Foundation /// - Event stream management /// - State synchronization /// -/// ## Usage -/// -/// ```swift -/// import AGUIClient -/// -/// let agent = HttpAgent(baseURL: agentURL) -/// for try await event in try await agent.run(input) { -/// // Process events -/// } -/// ``` public struct AGUIClient { /// The version of the AGUIClient module. public static let version = "0.1.0" diff --git a/Sources/AGUIClient/State/DefaultApplyEvents.swift b/Sources/AGUIClient/State/DefaultApplyEvents.swift index 55f9be8..aeb5280 100644 --- a/Sources/AGUIClient/State/DefaultApplyEvents.swift +++ b/Sources/AGUIClient/State/DefaultApplyEvents.swift @@ -12,26 +12,6 @@ extension AsyncSequence where Element == any AGUIEvent { /// triggering event. Callers should accumulate values from successive emissions /// to build the complete agent state. /// - /// ## Example - /// - /// ```swift - /// var currentMessages: [any Message] = [] - /// var currentState: State = Data("{}".utf8) - /// - /// for try await agentState in eventStream.applyEvents(input: input) { - /// if let messages = agentState.messages { - /// currentMessages = messages - /// } - /// if let state = agentState.state { - /// currentState = state - /// } - /// } - /// ``` - /// - /// - Parameters: - /// - input: The `RunAgentInput` that seeded this run, providing initial messages and state. - /// - subscribers: Optional list of subscribers to notify of events (reserved for future use). - /// - Returns: An `AsyncThrowingStream` of `AgentState` emissions. public func applyEvents( input: RunAgentInput, subscribers: [any AgentSubscriber] = [] diff --git a/Sources/AGUIClient/State/PatchApplicator.swift b/Sources/AGUIClient/State/PatchApplicator.swift index 6bb429b..0bd1588 100644 --- a/Sources/AGUIClient/State/PatchApplicator.swift +++ b/Sources/AGUIClient/State/PatchApplicator.swift @@ -8,40 +8,6 @@ import Foundation /// applying incremental changes to JSON documents. It supports all standard /// operations: add, remove, replace, move, copy, and test. /// -/// ## Usage -/// -/// ```swift -/// let applicator = PatchApplicator() -/// -/// let state = Data("{\"count\":5}".utf8) -/// let patch = Data(""" -/// [{"op":"replace","path":"/count","value":10}] -/// """.utf8) -/// -/// let newState = try applicator.apply(patch: patch, to: state) -/// ``` -/// -/// ## Supported Operations -/// -/// - `add`: Adds a value to an object or inserts into an array -/// - `remove`: Removes a value from an object or array -/// - `replace`: Replaces a value -/// - `move`: Moves a value from one location to another -/// - `copy`: Copies a value from one location to another -/// - `test`: Tests that a value equals the specified value -/// -/// ## Path Format (RFC 6901) -/// -/// Paths use JSON Pointer format: -/// - `/foo` - Root-level field "foo" -/// - `/foo/bar` - Nested field "bar" in object "foo" -/// - `/items/0` - First element of array "items" -/// - `/items/-` - Append to array "items" -/// - `/a~0b` - Field "a~b" (~ encoded as ~0) -/// - `/a~1b` - Field "a/b" (/ encoded as ~1) -/// -/// - SeeAlso: [RFC 6902 - JSON Patch](https://tools.ietf.org/html/rfc6902) -/// - SeeAlso: [RFC 6901 - JSON Pointer](https://tools.ietf.org/html/rfc6901) public struct PatchApplicator: Sendable { /// Errors that can occur during patch application. public enum PatchError: Error, LocalizedError { diff --git a/Sources/AGUIClient/State/StateManager.swift b/Sources/AGUIClient/State/StateManager.swift index d05ccc9..c33eda8 100644 --- a/Sources/AGUIClient/State/StateManager.swift +++ b/Sources/AGUIClient/State/StateManager.swift @@ -10,36 +10,6 @@ import Foundation /// - Incremental updates using JSON Patch (`STATE_DELTA` events) /// - State retrieval and reset /// -/// ## Usage -/// -/// ```swift -/// let manager = StateManager() -/// -/// // Handle snapshot event -/// await manager.handleSnapshot(snapshotEvent) -/// -/// // Handle delta event -/// try await manager.handleDelta(deltaEvent) -/// -/// // Get current state -/// let state = await manager.getState() -/// ``` -/// -/// ## Thread Safety -/// -/// `StateManager` is an actor, providing automatic thread safety for all -/// state operations. All methods can be safely called from multiple -/// concurrent tasks. -/// -/// ## State Lifecycle -/// -/// 1. Initialize with empty state `{}` -/// 2. Receive `STATE_SNAPSHOT` → full state replacement -/// 3. Receive `STATE_DELTA` → apply JSON Patch operations -/// 4. Query current state with `getState()` -/// 5. Reset to empty state with `reset()` -/// -/// - SeeAlso: `PatchApplicator`, `StateSnapshotEvent`, `StateDeltaEvent` public actor StateManager { /// Current application state as raw JSON data. private var currentState: Data diff --git a/Sources/AGUIClient/Streaming/AsyncSequence+Buffering.swift b/Sources/AGUIClient/Streaming/AsyncSequence+Buffering.swift index 2bfbdcf..e0c5a90 100644 --- a/Sources/AGUIClient/Streaming/AsyncSequence+Buffering.swift +++ b/Sources/AGUIClient/Streaming/AsyncSequence+Buffering.swift @@ -15,26 +15,6 @@ extension AsyncSequence where Self: Sendable, Element: Sendable { /// - strategy: Strategy for handling overflow /// - Returns: Buffered async sequence /// - /// ## Example - /// - /// ```swift - /// let buffered = eventStream.buffered(limit: 100, strategy: .dropOldest) - /// - /// for try await event in buffered { - /// // Buffer ensures max 100 events in memory - /// await slowProcessing(event) - /// } - /// ``` - /// - /// ## Memory Safety - /// - /// The buffer guarantees bounded memory usage: `limit * sizeof(Element)`. - /// - /// ## Performance - /// - /// - `.dropOldest`: O(1) append, O(n) drop (shifts elements) - /// - `.dropNewest`: O(1) append and drop - /// - `.suspend`: Natural backpressure (no buffer overhead) public func buffered( limit: Int, strategy: BufferingStrategy diff --git a/Sources/AGUIClient/Streaming/BufferingStrategy.swift b/Sources/AGUIClient/Streaming/BufferingStrategy.swift index 5a0d76b..1e771f1 100644 --- a/Sources/AGUIClient/Streaming/BufferingStrategy.swift +++ b/Sources/AGUIClient/Streaming/BufferingStrategy.swift @@ -7,11 +7,6 @@ import Foundation /// When a bounded buffer fills up and new elements arrive, the strategy /// determines which elements to keep and which to drop. /// -/// ## Usage -/// -/// ```swift -/// let buffered = stream.buffered(limit: 100, strategy: .dropOldest) -/// ``` public enum BufferingStrategy: Sendable { /// Drop the oldest elements when buffer is full. /// diff --git a/Sources/AGUIClient/Streaming/ChunkTransformer.swift b/Sources/AGUIClient/Streaming/ChunkTransformer.swift index 8441840..97d643e 100644 --- a/Sources/AGUIClient/Streaming/ChunkTransformer.swift +++ b/Sources/AGUIClient/Streaming/ChunkTransformer.swift @@ -23,23 +23,6 @@ public enum ChunkTransformError: Error, Sendable { /// This ensures downstream processing can rely on standard event sequences regardless /// of the upstream stream shape. /// -/// ## Behavior -/// -/// - **Text Chunks**: Transformed into TextMessageStart → TextMessageContent(s) → TextMessageEnd -/// - **Tool Chunks**: Transformed into ToolCallStart → ToolCallArgs(s) → ToolCallEnd -/// - **Existing Events**: Pass through unchanged -/// - **Mode Switching**: Automatically closes pending sequences when switching between text/tool modes -/// -/// ## Usage -/// -/// ```swift -/// let transformed = events.transformChunks() -/// for try await event in transformed { -/// // Process structured events -/// } -/// ``` -/// -/// - SeeAlso: ``ChunkTransformError`` public struct ChunkTransformer { /// Creates a new chunk transformer. public init() {} diff --git a/Sources/AGUIClient/Streaming/EventStream.swift b/Sources/AGUIClient/Streaming/EventStream.swift index 48c6e2a..c66dd9f 100644 --- a/Sources/AGUIClient/Streaming/EventStream.swift +++ b/Sources/AGUIClient/Streaming/EventStream.swift @@ -11,44 +11,6 @@ import Foundation /// 3. Decodes AG-UI events using AGUIEventDecoder /// 4. Handles errors gracefully /// -/// ## Usage -/// -/// ```swift -/// let transport = HttpTransport(configuration: config) -/// let bytes = try await transport.execute(endpoint: "/run", input: input) -/// let decoder = AGUIEventDecoder() -/// let stream = EventStream(bytes: bytes, decoder: decoder) -/// -/// for try await event in stream { -/// switch event.eventType { -/// case .textMessageChunk: -/// let chunk = event as! TextMessageChunkEvent -/// print(chunk.delta, terminator: "") -/// case .runFinished: -/// print("\nDone!") -/// default: -/// break -/// } -/// } -/// ``` -/// -/// ## Error Handling -/// -/// - Malformed JSON events are logged and skipped -/// - Unknown event types are returned as `UnknownEvent` -/// - UTF-8 decoding errors are handled gracefully -/// - Network errors propagate to the caller -/// -/// ## Last-Event-ID tracking -/// -/// `lastEventId` exposes the most recent `id:` field seen in the SSE stream. -/// It is updated as events arrive and can be read after a mid-stream failure -/// to resume from the correct position on reconnect. -/// -/// ## Thread Safety -/// -/// `EventStream` is Sendable and can be used across concurrency domains. -/// Each iteration creates a new iterator with isolated state. public struct EventStream: AsyncSequence where Bytes.Element == UInt8 { public typealias Element = any AGUIEvent diff --git a/Sources/AGUIClient/Streaming/SseEvent.swift b/Sources/AGUIClient/Streaming/SseEvent.swift index 4eff1da..1b1187e 100644 --- a/Sources/AGUIClient/Streaming/SseEvent.swift +++ b/Sources/AGUIClient/Streaming/SseEvent.swift @@ -7,29 +7,6 @@ import Foundation /// Server-Sent Events (SSE) is a standard for server-to-client streaming /// over HTTP. Each event can contain data, an ID for reconnection, and an /// event type. -/// -/// ## SSE Format -/// -/// ``` -/// event: notification -/// id: 123 -/// data: {"message":"hello"} -/// -/// ``` -/// -/// ## Example -/// -/// ```swift -/// let event = SseEvent( -/// data: "{\"type\":\"MESSAGE\"}", -/// id: "123", -/// event: "message" -/// ) -/// ``` -/// -/// ## Reference -/// -/// SSE specification: https://html.spec.whatwg.org/multipage/server-sent-events.html public struct SseEvent: Sendable, Equatable { /// The event data payload. /// diff --git a/Sources/AGUIClient/Streaming/SseParser.swift b/Sources/AGUIClient/Streaming/SseParser.swift index 82358ed..3a98025 100644 --- a/Sources/AGUIClient/Streaming/SseParser.swift +++ b/Sources/AGUIClient/Streaming/SseParser.swift @@ -8,49 +8,6 @@ import Foundation /// arbitrary chunks. It maintains an internal buffer for incomplete events /// and returns complete events as they become available. /// -/// ## Usage -/// -/// ```swift -/// var parser = SseParser() -/// -/// // Parse chunks as they arrive -/// for chunk in streamChunks { -/// let events = parser.parse(chunk) -/// for event in events { -/// print("Received: \(event.data)") -/// } -/// } -/// ``` -/// -/// ## SSE Format -/// -/// Server-Sent Events use a line-based format: -/// - Lines starting with `data:` contain the payload -/// - Lines starting with `id:` specify the event ID -/// - Lines starting with `event:` specify the event type -/// - Lines starting with `:` are comments (ignored) -/// - Empty line (double newline) signals end of event -/// - Multiple `data:` lines are concatenated with newlines -/// -/// ## Example Input -/// -/// ``` -/// data: {"type":"MESSAGE","content":"Hello"} -/// -/// event: notification -/// id: 123 -/// data: {"alert":"New message"} -/// -/// ``` -/// -/// ## Thread Safety -/// -/// `SseParser` is a mutable struct and not thread-safe. Each thread -/// should maintain its own parser instance. -/// -/// ## Reference -/// -/// SSE specification: https://html.spec.whatwg.org/multipage/server-sent-events.html public struct SseParser { /// Maximum number of UTF-8 bytes the internal buffer may hold. /// diff --git a/Sources/AGUIClient/Subscriber/AgentSubscriber.swift b/Sources/AGUIClient/Subscriber/AgentSubscriber.swift index 014b17e..72defba 100644 --- a/Sources/AGUIClient/Subscriber/AgentSubscriber.swift +++ b/Sources/AGUIClient/Subscriber/AgentSubscriber.swift @@ -10,21 +10,6 @@ import Foundation /// Subscribers can replace the pending message collection, update state, or /// stop propagation so the default handlers skip their own processing. /// -/// ## Example -/// -/// ```swift -/// // Append a custom message -/// func onRunInitialized(params: AgentSubscriberParams) async -> AgentStateMutation? { -/// var updatedMessages = params.messages -/// updatedMessages.append(SystemMessage( -/// id: "custom-prompt", -/// content: "Be concise and helpful." -/// )) -/// return AgentStateMutation(messages: updatedMessages) -/// } -/// ``` -/// -/// - SeeAlso: ``AgentSubscriber`` public struct AgentStateMutation: Sendable { /// Replacement messages for the agent's conversation history. /// diff --git a/Sources/AGUIClient/Transport/HTTPClient.swift b/Sources/AGUIClient/Transport/HTTPClient.swift index 8d85957..9abc38b 100644 --- a/Sources/AGUIClient/Transport/HTTPClient.swift +++ b/Sources/AGUIClient/Transport/HTTPClient.swift @@ -8,25 +8,6 @@ import Foundation /// and testability. Implementations can use URLSession, mock responses, /// or custom networking stacks. /// -/// ## Example Implementation -/// -/// ```swift -/// actor URLSessionHTTPClient: HTTPClient { -/// private let session: URLSession -/// -/// init(session: URLSession) { -/// self.session = session -/// } -/// -/// func execute(_ request: URLRequest) async throws -> HTTPResponse { -/// let (bytes, urlResponse) = try await session.bytes(for: request) -/// guard let httpResponse = urlResponse as? HTTPURLResponse else { -/// throw ClientError.invalidResponse -/// } -/// return HTTPResponse(bytes: bytes, httpResponse: httpResponse) -/// } -/// } -/// ``` public protocol HTTPClient: Sendable { /// Executes an HTTP request and returns the response. /// diff --git a/Sources/AGUIClient/Transport/HttpTransport.swift b/Sources/AGUIClient/Transport/HttpTransport.swift index b00f784..e0cc688 100644 --- a/Sources/AGUIClient/Transport/HttpTransport.swift +++ b/Sources/AGUIClient/Transport/HttpTransport.swift @@ -9,23 +9,6 @@ import Foundation /// dependency injection for testability and flexibility. It accepts any /// `HTTPClient` implementation, defaulting to `URLSessionHTTPClient`. /// -/// ## Example -/// -/// ```swift -/// // Default usage with URLSession -/// let config = HttpAgentConfiguration(baseURL: agentURL) -/// let transport = HttpTransport(configuration: config) -/// -/// // With custom HTTP client (e.g., for testing) -/// let mockClient = MockHTTPClient() -/// let transport = HttpTransport( -/// configuration: config, -/// httpClient: mockClient -/// ) -/// -/// // Execute request -/// let bytes = try await transport.execute(endpoint: "/run", input: input) -/// ``` public actor HttpTransport { private let httpClient: any HTTPClient private let configuration: HttpAgentConfiguration diff --git a/Sources/AGUIClient/Transport/URLSessionHTTPClient.swift b/Sources/AGUIClient/Transport/URLSessionHTTPClient.swift index 33896eb..f516447 100644 --- a/Sources/AGUIClient/Transport/URLSessionHTTPClient.swift +++ b/Sources/AGUIClient/Transport/URLSessionHTTPClient.swift @@ -8,22 +8,6 @@ import Foundation /// URLSession for networking. It supports full URLSession configuration /// and can be injected with a custom session for testing. /// -/// ## Example -/// -/// ```swift -/// // Default usage -/// let client = URLSessionHTTPClient.create() -/// -/// // Custom configuration -/// let config = URLSessionConfiguration.default -/// config.timeoutIntervalForRequest = 30 -/// let session = URLSession(configuration: config) -/// let client = URLSessionHTTPClient(session: session) -/// -/// // Execute request -/// let request = URLRequest(url: url) -/// let response = try await client.execute(request) -/// ``` public actor URLSessionHTTPClient: HTTPClient { private let session: URLSession diff --git a/Sources/AGUICore/AGUICore.swift b/Sources/AGUICore/AGUICore.swift deleted file mode 100644 index 11cb377..0000000 --- a/Sources/AGUICore/AGUICore.swift +++ /dev/null @@ -1 +0,0 @@ -// Copyright (c) 2025 Perfect Aduh. MIT License. See LICENSE for details. diff --git a/Sources/AGUICore/Decoding/AGUIEventDecoder.swift b/Sources/AGUICore/Decoding/AGUIEventDecoder.swift index c8a665e..87a1f65 100644 --- a/Sources/AGUICore/Decoding/AGUIEventDecoder.swift +++ b/Sources/AGUICore/Decoding/AGUIEventDecoder.swift @@ -4,86 +4,6 @@ import Foundation /// Decoder for AG-UI protocol events with polymorphic deserialization. /// -/// `AGUIEventDecoder` decodes JSON event data into strongly-typed event objects based on -/// the "type" field in the JSON. It uses a registry-based architecture that allows you to -/// customize which event types are supported and how unknown events are handled. -/// -/// ## Basic Usage -/// -/// ```swift -/// // Create a decoder with default settings (strict mode) -/// let decoder = AGUIEventDecoder() -/// -/// // Decode an event from JSON data -/// let event = try decoder.decode(jsonData) -/// -/// // Pattern match on the event type -/// switch event.eventType { -/// case .runStarted: -/// let runStarted = event as! RunStartedEvent -/// print("Run started: \(runStarted.runId)") -/// case .runFinished: -/// let runFinished = event as! RunFinishedEvent -/// print("Run finished: \(runFinished.runId)") -/// default: -/// print("Other event: \(event.eventType)") -/// } -/// ``` -/// -/// ## Configuration Modes -/// -/// ### Strict Mode (Default) -/// -/// In strict mode, unknown or unsupported events throw errors: -/// -/// ```swift -/// let decoder = AGUIEventDecoder() // Default: .throwError -/// // Throws EventDecodingError.unknownEventType for unrecognized types -/// ``` -/// -/// ### Tolerant Mode -/// -/// In tolerant mode, unknown events are returned as `UnknownEvent`: -/// -/// ```swift -/// var config = AGUIEventDecoder.Configuration() -/// config.unknownEventStrategy = .returnUnknown -/// let decoder = AGUIEventDecoder(config: config) -/// -/// let event = try decoder.decode(data) -/// if let unknown = event as? UnknownEvent { -/// print("Unknown event type: \(unknown.typeRaw)") -/// // Can still access raw JSON for forwarding or logging -/// } -/// ``` -/// -/// ## Custom Registries -/// -/// You can provide a custom registry to control which event types are supported: -/// -/// ```swift -/// let customRegistry: [EventType: AGUIEventDecoder.DecodeHandler] = [ -/// .runStarted: { data, decoder in -/// try decoder.decode(RunStartedEventDTO.self, from: data).toDomain(rawEvent: data) -/// } -/// // Add more handlers as needed -/// ] -/// -/// let decoder = AGUIEventDecoder(registry: customRegistry) -/// ``` -/// -/// ## Error Handling -/// -/// The decoder throws `EventDecodingError` for various failure scenarios: -/// -/// - `.missingTypeField`: The JSON is missing the required "type" field -/// - `.invalidJSON`: The JSON data is malformed or invalid -/// - `.unknownEventType(String)`: The event type is not recognized (strict mode only) -/// - `.unsupportedEventType(EventType)`: The event type is known but has no handler (strict mode only) -/// - `.decodingFailed(String)`: Field-level decoding errors with detailed messages -/// -/// ## Thread Safety -/// /// `AGUIEventDecoder` is thread-safe and can be used concurrently. The decoder itself /// is immutable after initialization, and all configuration is `Sendable`. /// @@ -110,12 +30,6 @@ public struct AGUIEventDecoder: Sendable { /// /// Use `Configuration` to customize decoder behavior, particularly how unknown /// or unsupported events are handled. - /// - /// ```swift - /// var config = AGUIEventDecoder.Configuration() - /// config.unknownEventStrategy = .returnUnknown - /// let decoder = AGUIEventDecoder(config: config) - /// ``` public struct Configuration: Sendable { /// Strategy for handling unknown or unsupported event types. /// @@ -165,16 +79,6 @@ public struct AGUIEventDecoder: Sendable { /// The decoder uses the provided registry to determine which event types can be decoded. /// If no registry is provided, it uses `defaultRegistry()` which includes all lifecycle events. /// - /// Example with custom JSON decoder: - /// ```swift - /// let decoder = AGUIEventDecoder( - /// makeDecoder: { - /// let jsonDecoder = JSONDecoder() - /// jsonDecoder.dateDecodingStrategy = .millisecondsSince1970 - /// return jsonDecoder - /// } - /// ) - /// ``` public init( config: Configuration = .init(), makeDecoder: @escaping @Sendable () -> JSONDecoder = { @@ -202,23 +106,6 @@ public struct AGUIEventDecoder: Sendable { /// - Returns: A decoded `AGUIEvent` instance (specific type depends on the "type" field) /// - Throws: `EventDecodingError` if decoding fails or the event type is unknown/unsupported (in strict mode) /// - /// Example: - /// ```swift - /// let jsonData = """ - /// { - /// "type": "RUN_STARTED", - /// "threadId": "thread-123", - /// "runId": "run-456" - /// } - /// """.data(using: .utf8)! - /// - /// let decoder = AGUIEventDecoder() - /// let event = try decoder.decode(jsonData) - /// - /// if let runStarted = event as? RunStartedEvent { - /// print("Run \(runStarted.runId) started in thread \(runStarted.threadId)") - /// } - /// ``` public func decode(_ data: Data) throws -> any AGUIEvent { let decoder = makeDecoder() diff --git a/Sources/AGUICore/Decoding/MessageDecoder.swift b/Sources/AGUICore/Decoding/MessageDecoder.swift index 4583424..ebb2aba 100644 --- a/Sources/AGUICore/Decoding/MessageDecoder.swift +++ b/Sources/AGUICore/Decoding/MessageDecoder.swift @@ -8,58 +8,6 @@ import Foundation /// the "role" field in the JSON. It uses a registry-based architecture matching the pattern /// used by `AGUIEventDecoder`. /// -/// ## Basic Usage -/// -/// ```swift -/// // Create a decoder with default registry -/// let decoder = MessageDecoder() -/// -/// // Decode a message from JSON data -/// let message = try decoder.decode(jsonData) -/// -/// // Pattern match on the message role -/// switch message.role { -/// case .user: -/// let userMessage = message as! UserMessage -/// print("User said: \(userMessage.content)") -/// case .assistant: -/// let assistantMessage = message as! AssistantMessage -/// print("Assistant replied: \(assistantMessage.content ?? "")") -/// default: -/// print("Other message type: \(message.role)") -/// } -/// ``` -/// -/// ## Custom Registries -/// -/// You can provide a custom registry to control which message types are supported: -/// -/// ```swift -/// let customRegistry: [Role: MessageDecoder.DecodeHandler] = [ -/// .user: { data, decoder in -/// try UserMessageDTO.decode(from: data, decoder: decoder).toDomain() -/// } -/// // Add more handlers as needed -/// ] -/// -/// let decoder = MessageDecoder(registry: customRegistry) -/// ``` -/// -/// ## Error Handling -/// -/// The decoder throws `MessageDecodingError` for various failure scenarios: -/// -/// - `.missingRoleField`: The JSON is missing the required "role" field -/// - `.invalidJSON`: The JSON data is malformed or invalid -/// - `.unknownRole(String)`: The role value is not recognized -/// - `.unsupportedRole(Role)`: The role is known but has no handler -/// - `.decodingFailed(String)`: Field-level decoding errors with detailed messages -/// -/// ## Thread Safety -/// -/// `MessageDecoder` is thread-safe and can be used concurrently. The decoder itself -/// is immutable after initialization. -/// /// - SeeAlso: `Message`, `Role`, `MessageDecodingError` public struct MessageDecoder: Sendable { diff --git a/Sources/AGUICore/Encoding/MessageEncoder.swift b/Sources/AGUICore/Encoding/MessageEncoder.swift index 9f89d01..cfcfa20 100644 --- a/Sources/AGUICore/Encoding/MessageEncoder.swift +++ b/Sources/AGUICore/Encoding/MessageEncoder.swift @@ -8,50 +8,6 @@ import Foundation /// the message's role. It uses a registry-based architecture matching the pattern /// used by `MessageDecoder`. /// -/// ## Basic Usage -/// -/// ```swift -/// // Create an encoder with default registry -/// let encoder = MessageEncoder() -/// -/// // Encode a message to JSON data -/// let message = UserMessage(id: "msg-1", content: "Hello!") -/// let jsonData = try encoder.encode(message) -/// -/// // Convert to string for viewing -/// let jsonString = String(data: jsonData, encoding: .utf8) -/// ``` -/// -/// ## Custom Registries -/// -/// You can provide a custom registry to control how message types are encoded: -/// -/// ```swift -/// let customRegistry: [Role: MessageEncoder.EncodeHandler] = [ -/// .user: { message, encoder in -/// let userMessage = message as! UserMessage -/// // Custom encoding logic -/// return customEncoding(userMessage) -/// } -/// ] -/// -/// let encoder = MessageEncoder(registry: customRegistry) -/// ``` -/// -/// ## Error Handling -/// -/// The encoder throws `MessageEncodingError` for various failure scenarios: -/// -/// - `.unsupportedRole(Role)`: The message role has no registered encoder -/// - `.invalidMessageType(Role, String)`: Message type doesn't match its role -/// - `.encodingFailed(String)`: Field-level encoding errors -/// -/// ## Thread Safety -/// -/// `MessageEncoder` is thread-safe and can be used concurrently. The encoder itself -/// is immutable after initialization. -/// -/// - SeeAlso: `Message`, `Role`, `MessageDecoder` public struct MessageEncoder: Sendable { /// Handler function type for encoding a specific message type. diff --git a/Sources/AGUICore/EventType.swift b/Sources/AGUICore/EventType.swift index c1343d8..a851235 100644 --- a/Sources/AGUICore/EventType.swift +++ b/Sources/AGUICore/EventType.swift @@ -15,7 +15,7 @@ import Foundation /// - **Reasoning**: `reasoningStart`, `reasoningMessageStart`, `reasoningMessageContent`, `reasoningMessageEnd`, `reasoningMessageChunk`, `reasoningEnd`, `reasoningEncryptedValue` /// - **Activity**: `activitySnapshot`, `activityDelta` /// - **Special**: `raw`, `custom` - +/// public enum EventType: String, Codable, CaseIterable, Sendable { // MARK: - Lifecycle Events (5) diff --git a/Sources/AGUICore/Events/AGUIEvent.swift b/Sources/AGUICore/Events/AGUIEvent.swift index 33f5bef..31781a7 100644 --- a/Sources/AGUICore/Events/AGUIEvent.swift +++ b/Sources/AGUICore/Events/AGUIEvent.swift @@ -23,7 +23,6 @@ import Foundation /// Uses polymorphic serialization where the "type" field determines which /// specific event type to deserialize to. /// -/// - SeeAlso: `EventType` public protocol AGUIEvent: Sendable { /// The type of this event. /// diff --git a/Sources/AGUICore/Events/ActivityEvents/ActivityDeltaEvent.swift b/Sources/AGUICore/Events/ActivityEvents/ActivityDeltaEvent.swift index 9a9396b..7d844e7 100644 --- a/Sources/AGUICore/Events/ActivityEvents/ActivityDeltaEvent.swift +++ b/Sources/AGUICore/Events/ActivityEvents/ActivityDeltaEvent.swift @@ -8,8 +8,6 @@ import Foundation /// RFC 6902 JSON Patch format. It allows efficient updates to structured /// activity content without sending the full content each time. /// -/// - SeeAlso: `ActivitySnapshotEvent`, `StateDeltaEvent` -/// - SeeAlso: [RFC 6902 - JSON Patch](https://tools.ietf.org/html/rfc6902) public struct ActivityDeltaEvent: AGUIEvent, Equatable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ActivityEvents/ActivitySnapshotEvent.swift b/Sources/AGUICore/Events/ActivityEvents/ActivitySnapshotEvent.swift index ae10365..f91ab2b 100644 --- a/Sources/AGUICore/Events/ActivityEvents/ActivitySnapshotEvent.swift +++ b/Sources/AGUICore/Events/ActivityEvents/ActivitySnapshotEvent.swift @@ -8,7 +8,6 @@ import Foundation /// the standard text/tool paradigm, such as A2UI surfaces. The content field /// contains activity-type-specific data. /// -/// - SeeAlso: `ActivityDeltaEvent` public struct ActivitySnapshotEvent: AGUIEvent, Equatable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/LifeCycleEvents/RunErrorEvent.swift b/Sources/AGUICore/Events/LifeCycleEvents/RunErrorEvent.swift index 4dc0293..a495456 100644 --- a/Sources/AGUICore/Events/LifeCycleEvents/RunErrorEvent.swift +++ b/Sources/AGUICore/Events/LifeCycleEvents/RunErrorEvent.swift @@ -7,7 +7,6 @@ import Foundation /// This event is emitted when an agent run fails due to an unrecoverable error. /// It provides error details and optional error codes for debugging and handling. /// -/// - SeeAlso: `RunStartedEvent`, `RunFinishedEvent` public struct RunErrorEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/LifeCycleEvents/RunFinishedEvent.swift b/Sources/AGUICore/Events/LifeCycleEvents/RunFinishedEvent.swift index fc949b2..335a77e 100644 --- a/Sources/AGUICore/Events/LifeCycleEvents/RunFinishedEvent.swift +++ b/Sources/AGUICore/Events/LifeCycleEvents/RunFinishedEvent.swift @@ -7,7 +7,6 @@ import Foundation /// This event is emitted when an agent has finished processing a run request /// and has generated all output. It signals the end of the execution lifecycle. /// -/// - SeeAlso: `RunStartedEvent`, `RunErroredEvent` public struct RunFinishedEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/LifeCycleEvents/RunStartedEvent.swift b/Sources/AGUICore/Events/LifeCycleEvents/RunStartedEvent.swift index 70165ee..fc8ec36 100644 --- a/Sources/AGUICore/Events/LifeCycleEvents/RunStartedEvent.swift +++ b/Sources/AGUICore/Events/LifeCycleEvents/RunStartedEvent.swift @@ -7,7 +7,6 @@ import Foundation /// This event is emitted when an agent begins processing a new run request. /// It provides the thread and run identifiers that will be used throughout /// the execution lifecycle. -/// - SeeAlso: `RunFinishedEvent`, `RunErrorEvent` public struct RunStartedEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ReasoningEvents/ReasoningEndEvent.swift b/Sources/AGUICore/Events/ReasoningEvents/ReasoningEndEvent.swift index eeeb9f1..9b664a8 100644 --- a/Sources/AGUICore/Events/ReasoningEvents/ReasoningEndEvent.swift +++ b/Sources/AGUICore/Events/ReasoningEvents/ReasoningEndEvent.swift @@ -5,10 +5,8 @@ import Foundation /// Event marking the end of a reasoning phase. /// /// This event signals that the agent has completed its internal reasoning process -/// for the message identified by `messageId`. It is the replacement for the -/// deprecated ``ThinkingEndEvent``. +/// for the message identified by `messageId`. /// -/// - SeeAlso: ``ReasoningStartEvent``, ``ReasoningMessageEndEvent`` public struct ReasoningEndEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageChunkEvent.swift b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageChunkEvent.swift index d310421..ae50e2a 100644 --- a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageChunkEvent.swift +++ b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageChunkEvent.swift @@ -9,7 +9,6 @@ import Foundation /// start/content/end events. Both fields are optional; at least one should be non-nil /// for the event to be meaningful. /// -/// - SeeAlso: ``ReasoningMessageStartEvent``, ``ReasoningMessageContentEvent``, ``ReasoningMessageEndEvent`` public struct ReasoningMessageChunkEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageContentEvent.swift b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageContentEvent.swift index c829881..c5341c0 100644 --- a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageContentEvent.swift +++ b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageContentEvent.swift @@ -5,8 +5,7 @@ import Foundation /// Event containing a streaming chunk of reasoning message content. /// /// This event delivers an incremental piece of reasoning text during a reasoning -/// message's lifecycle. It is the replacement for the deprecated -/// ``ThinkingTextMessageContentEvent``. +/// message's lifecycle. /// /// - SeeAlso: ``ReasoningMessageStartEvent``, ``ReasoningMessageEndEvent`` public struct ReasoningMessageContentEvent: AGUIEvent, Equatable, Hashable, Sendable { diff --git a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageEndEvent.swift b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageEndEvent.swift index e872ea7..a8a1a68 100644 --- a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageEndEvent.swift +++ b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageEndEvent.swift @@ -5,7 +5,6 @@ import Foundation /// Event indicating the end of a streaming reasoning message. /// /// This event marks the completion of a reasoning message within a reasoning phase. -/// It is the replacement for the deprecated ``ThinkingTextMessageEndEvent``. /// /// - SeeAlso: ``ReasoningMessageStartEvent``, ``ReasoningMessageContentEvent`` public struct ReasoningMessageEndEvent: AGUIEvent, Equatable, Hashable, Sendable { diff --git a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageStartEvent.swift b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageStartEvent.swift index 191165d..254566e 100644 --- a/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageStartEvent.swift +++ b/Sources/AGUICore/Events/ReasoningEvents/ReasoningMessageStartEvent.swift @@ -5,8 +5,7 @@ import Foundation /// Event indicating the start of a streaming reasoning message. /// /// This event marks the beginning of a reasoning message within a reasoning phase. -/// The `role` field will always be `"reasoning"`. It is the replacement for the -/// deprecated ``ThinkingTextMessageStartEvent``. +/// The `role` field will always be `"reasoning"`. /// /// - SeeAlso: ``ReasoningMessageContentEvent``, ``ReasoningMessageEndEvent`` public struct ReasoningMessageStartEvent: AGUIEvent, Equatable, Hashable, Sendable { diff --git a/Sources/AGUICore/Events/ReasoningEvents/ReasoningStartEvent.swift b/Sources/AGUICore/Events/ReasoningEvents/ReasoningStartEvent.swift index eabe87a..cb710a0 100644 --- a/Sources/AGUICore/Events/ReasoningEvents/ReasoningStartEvent.swift +++ b/Sources/AGUICore/Events/ReasoningEvents/ReasoningStartEvent.swift @@ -5,8 +5,7 @@ import Foundation /// Event marking the start of a reasoning phase. /// /// This event signals that the agent has begun its internal reasoning process -/// for the message identified by `messageId`. It is the replacement for the -/// deprecated ``ThinkingStartEvent``. +/// for the message identified by `messageId`. /// /// - SeeAlso: ``ReasoningEndEvent``, ``ReasoningMessageStartEvent`` public struct ReasoningStartEvent: AGUIEvent, Equatable, Hashable, Sendable { diff --git a/Sources/AGUICore/Events/SpecialEvents/CustomEvent.swift b/Sources/AGUICore/Events/SpecialEvents/CustomEvent.swift index 318b592..94653a2 100644 --- a/Sources/AGUICore/Events/SpecialEvents/CustomEvent.swift +++ b/Sources/AGUICore/Events/SpecialEvents/CustomEvent.swift @@ -13,11 +13,6 @@ import Foundation /// - Extending the protocol with custom behavior /// - Handling domain-specific events (e.g., "com.example.analytics.pageView") /// -/// Custom event type identifiers typically follow reverse-domain notation -/// (e.g., "com.myapp.analytics.pageView", "org.example.userAction.buttonClick") -/// to ensure uniqueness across applications. -/// -/// - SeeAlso: `RawEvent`, `UnknownEvent` public struct CustomEvent: AGUIEvent, Equatable, Sendable { // MARK: - Properties @@ -26,11 +21,7 @@ public struct CustomEvent: AGUIEvent, Equatable, Sendable { /// /// This string uniquely identifies the type of custom event. It's recommended /// to use reverse-domain notation for globally unique identifiers. - /// - /// Examples: - /// - "com.example.userAction" - /// - "org.myapp.analytics.pageView" - /// - "simple.message" + public let name: String /// The custom event payload as raw JSON, matching the AG-UI protocol `value` wire field. diff --git a/Sources/AGUICore/Events/SpecialEvents/RawEvent.swift b/Sources/AGUICore/Events/SpecialEvents/RawEvent.swift index aa64e6b..6c888ba 100644 --- a/Sources/AGUICore/Events/SpecialEvents/RawEvent.swift +++ b/Sources/AGUICore/Events/SpecialEvents/RawEvent.swift @@ -13,7 +13,6 @@ import Foundation /// - Implementing custom event handling logic /// - Preserving raw event data for debugging or logging purposes /// -/// - SeeAlso: `CustomEvent`, `UnknownEvent` public struct RawEvent: AGUIEvent, Equatable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/StateEvents/MessagesSnapshotEvent.swift b/Sources/AGUICore/Events/StateEvents/MessagesSnapshotEvent.swift index 224cad1..59099dd 100644 --- a/Sources/AGUICore/Events/StateEvents/MessagesSnapshotEvent.swift +++ b/Sources/AGUICore/Events/StateEvents/MessagesSnapshotEvent.swift @@ -8,7 +8,6 @@ import Foundation /// history at a point in time. The messages are stored as raw JSON data to preserve /// their exact structure and allow for flexible message schemas. /// -/// - SeeAlso: `StateSnapshotEvent`, `StateDeltaEvent` public struct MessagesSnapshotEvent: AGUIEvent, Equatable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/StateEvents/StateSnapshotEvent.swift b/Sources/AGUICore/Events/StateEvents/StateSnapshotEvent.swift index 0ef7bf2..28a2e0b 100644 --- a/Sources/AGUICore/Events/StateEvents/StateSnapshotEvent.swift +++ b/Sources/AGUICore/Events/StateEvents/StateSnapshotEvent.swift @@ -8,7 +8,6 @@ import Foundation /// or reset the application state. The snapshot is stored as raw JSON data /// to preserve its exact structure and allow for flexible state schemas. /// -/// - SeeAlso: `StateDeltaEvent`, `MessagesSnapshotEvent` public struct StateSnapshotEvent: AGUIEvent, Equatable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/TextMessageEvents/TextMessageChunkEvent.swift b/Sources/AGUICore/Events/TextMessageEvents/TextMessageChunkEvent.swift index fd327eb..cfc0494 100644 --- a/Sources/AGUICore/Events/TextMessageEvents/TextMessageChunkEvent.swift +++ b/Sources/AGUICore/Events/TextMessageEvents/TextMessageChunkEvent.swift @@ -9,7 +9,6 @@ import Foundation /// `TextMessageChunkEvent` can automatically start and end text message sequences /// when no text message is currently active. /// -/// - SeeAlso: `TextMessageStartEvent`, `TextMessageContentEvent`, `TextMessageEndEvent` public struct TextMessageChunkEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/TextMessageEvents/TextMessageContentEvent.swift b/Sources/AGUICore/Events/TextMessageEvents/TextMessageContentEvent.swift index b09bf07..144cc32 100644 --- a/Sources/AGUICore/Events/TextMessageEvents/TextMessageContentEvent.swift +++ b/Sources/AGUICore/Events/TextMessageEvents/TextMessageContentEvent.swift @@ -10,7 +10,6 @@ import Foundation /// `messageId` field, which matches the ID from the corresponding /// `TextMessageStartEvent`. /// -/// - SeeAlso: `TextMessageStartEvent`, `TextMessageEndEvent`, `TextMessageChunkEvent` public struct TextMessageContentEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/TextMessageEvents/TextMessageEndEvent.swift b/Sources/AGUICore/Events/TextMessageEvents/TextMessageEndEvent.swift index c77e20a..14b27e0 100644 --- a/Sources/AGUICore/Events/TextMessageEvents/TextMessageEndEvent.swift +++ b/Sources/AGUICore/Events/TextMessageEvents/TextMessageEndEvent.swift @@ -9,7 +9,6 @@ import Foundation /// the message via the `messageId` field, which matches the ID from the /// corresponding `TextMessageStartEvent`. /// -/// - SeeAlso: `TextMessageStartEvent`, `TextMessageContentEvent`, `TextMessageChunkEvent` public struct TextMessageEndEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/TextMessageEvents/TextMessageStartEvent.swift b/Sources/AGUICore/Events/TextMessageEvents/TextMessageStartEvent.swift index ee8555f..367157b 100644 --- a/Sources/AGUICore/Events/TextMessageEvents/TextMessageStartEvent.swift +++ b/Sources/AGUICore/Events/TextMessageEvents/TextMessageStartEvent.swift @@ -8,7 +8,6 @@ import Foundation /// It provides the message identifier that will be used to associate subsequent /// content chunks and the end event with this message. /// -/// - SeeAlso: `TextMessageContentEvent`, `TextMessageEndEvent`, `TextMessageChunkEvent` public struct TextMessageStartEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ToolCallEvents/ToolCallArgsEvent.swift b/Sources/AGUICore/Events/ToolCallEvents/ToolCallArgsEvent.swift index 682d668..1ea26cc 100644 --- a/Sources/AGUICore/Events/ToolCallEvents/ToolCallArgsEvent.swift +++ b/Sources/AGUICore/Events/ToolCallEvents/ToolCallArgsEvent.swift @@ -9,7 +9,6 @@ import Foundation /// non-empty string. These events are associated with a tool call via the `toolCallId` /// field, which matches the ID from the corresponding `ToolCallStartEvent`. /// -/// - SeeAlso: `ToolCallStartEvent`, `ToolCallEndEvent`, `ToolCallResultEvent`, `ToolCallChunkEvent` public struct ToolCallArgsEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ToolCallEvents/ToolCallChunkEvent.swift b/Sources/AGUICore/Events/ToolCallEvents/ToolCallChunkEvent.swift index c694836..83c82fa 100644 --- a/Sources/AGUICore/Events/ToolCallEvents/ToolCallChunkEvent.swift +++ b/Sources/AGUICore/Events/ToolCallEvents/ToolCallChunkEvent.swift @@ -9,7 +9,6 @@ import Foundation /// `ToolCallChunkEvent` can automatically start and end tool call sequences /// when no tool call is currently active. /// -/// - SeeAlso: `ToolCallStartEvent`, `ToolCallArgsEvent`, `ToolCallEndEvent`, `ToolCallResultEvent` public struct ToolCallChunkEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ToolCallEvents/ToolCallEndEvent.swift b/Sources/AGUICore/Events/ToolCallEvents/ToolCallEndEvent.swift index 2c677f3..3398980 100644 --- a/Sources/AGUICore/Events/ToolCallEvents/ToolCallEndEvent.swift +++ b/Sources/AGUICore/Events/ToolCallEvents/ToolCallEndEvent.swift @@ -8,7 +8,6 @@ import Foundation /// of the tool call process and is associated with the tool call via the `toolCallId` /// field, which matches the ID from the corresponding `ToolCallStartEvent`. /// -/// - SeeAlso: `ToolCallStartEvent`, `ToolCallArgsEvent`, `ToolCallResultEvent`, `ToolCallChunkEvent` public struct ToolCallEndEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ToolCallEvents/ToolCallResultEvent.swift b/Sources/AGUICore/Events/ToolCallEvents/ToolCallResultEvent.swift index cb12d9d..b628eb7 100644 --- a/Sources/AGUICore/Events/ToolCallEvents/ToolCallResultEvent.swift +++ b/Sources/AGUICore/Events/ToolCallEvents/ToolCallResultEvent.swift @@ -8,7 +8,6 @@ import Foundation /// It provides the tool call result content and associates it with both the /// tool call (via `toolCallId`) and the conversation message (via `messageId`). /// -/// - SeeAlso: `ToolCallStartEvent`, `ToolCallArgsEvent`, `ToolCallEndEvent`, `ToolCallChunkEvent` public struct ToolCallResultEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/ToolCallEvents/ToolCallStartEvent.swift b/Sources/AGUICore/Events/ToolCallEvents/ToolCallStartEvent.swift index 0c20b37..2434a13 100644 --- a/Sources/AGUICore/Events/ToolCallEvents/ToolCallStartEvent.swift +++ b/Sources/AGUICore/Events/ToolCallEvents/ToolCallStartEvent.swift @@ -8,7 +8,6 @@ import Foundation /// tool call identifier and name that will be used to associate subsequent /// argument chunks, the result, and the end event with this tool call. /// -/// - SeeAlso: `ToolCallArgsEvent`, `ToolCallEndEvent`, `ToolCallResultEvent`, `ToolCallChunkEvent` public struct ToolCallStartEvent: AGUIEvent, Equatable, Hashable, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Events/UnknownEvent.swift b/Sources/AGUICore/Events/UnknownEvent.swift index 2524761..84b4048 100644 --- a/Sources/AGUICore/Events/UnknownEvent.swift +++ b/Sources/AGUICore/Events/UnknownEvent.swift @@ -8,49 +8,6 @@ import Foundation /// (`unknownEventStrategy = .returnUnknown`) and encounters an event that cannot be /// decoded into a known event type. /// -/// ## When UnknownEvent is Created -/// -/// An `UnknownEvent` is created in two scenarios: -/// -/// 1. **Unknown Event Type**: The event's "type" field contains a value that is not -/// recognized by the `EventType` enum (e.g., a future protocol extension or custom type). -/// -/// 2. **Unsupported Event Type**: The event type is recognized but no decoder handler -/// is registered for it in the decoder's registry (e.g., a known type that hasn't -/// been implemented yet). -/// -/// ## Usage -/// -/// ```swift -/// var config = AGUIEventDecoder.Configuration() -/// config.unknownEventStrategy = .returnUnknown -/// let decoder = AGUIEventDecoder(config: config) -/// -/// let event = try decoder.decode(data) -/// if let unknown = event as? UnknownEvent { -/// print("Received unknown event type: \(unknown.typeRaw)") -/// // Access raw JSON for inspection or forwarding -/// if let rawData = unknown.rawEvent { -/// let json = try JSONSerialization.jsonObject(with: rawData) -/// // Handle or forward the unknown event -/// } -/// } -/// ``` -/// -/// ## Properties -/// -/// - `typeRaw`: The raw string value of the "type" field from the JSON -/// - `rawEvent`: The complete original JSON data, preserved for inspection or forwarding -/// - `eventType`: Always returns `.unknown` — distinct from the genuine `.raw` wire event -/// - `timestamp`: Always returns `nil` since unknown events cannot be parsed for timestamps -/// -/// ## Forward Compatibility -/// -/// Using `UnknownEvent` enables forward compatibility with protocol extensions. When -/// new event types are added to the AG-UI protocol, older SDK versions can still -/// receive and forward these events without crashing, even if they can't decode them. -/// -/// - SeeAlso: `AGUIEventDecoder`, `EventDecodingError`, `EventType` public struct UnknownEvent: AGUIEvent, Sendable { // MARK: - Properties diff --git a/Sources/AGUICore/Types/AgentExecution/Context.swift b/Sources/AGUICore/Types/AgentExecution/Context.swift index eb72f5f..9ff1950 100644 --- a/Sources/AGUICore/Types/AgentExecution/Context.swift +++ b/Sources/AGUICore/Types/AgentExecution/Context.swift @@ -23,48 +23,12 @@ import Foundation /// - `description`: Human-readable description of what the context represents /// - `value`: The actual context value as a string /// -/// ## Usage Examples -/// -/// ```swift -/// // User preference context -/// let themeContext = Context( -/// description: "User theme preference", -/// value: "dark" -/// ) -/// -/// // Location context -/// let locationContext = Context( -/// description: "User location", -/// value: "San Francisco, CA" -/// ) -/// -/// // Multiple contexts in RunAgentInput -/// let input = RunAgentInput( -/// threadId: "thread-123", -/// runId: "run-456", -/// context: [themeContext, locationContext] -/// ) -/// ``` -/// -/// ## Value Encoding -/// -/// The `value` field is a string, but it can contain: -/// - Simple values: `"dark"`, `"en-US"`, `"12345"` -/// - Structured data as JSON strings: `"{\"name\": \"Alice\", \"role\": \"admin\"}"` -/// - Timestamps: `"2024-01-01T12:00:00Z"` -/// - Any other string-encoded data -/// -/// - SeeAlso: ``RunAgentInput`` public struct Context: Sendable, Codable, Hashable { /// A human-readable description of the context. /// /// This field describes what the context value represents, /// helping agents understand how to interpret the value. /// - /// Examples: - /// - `"User theme preference"` - /// - `"API authentication token"` - /// - `"Current timezone"` public let description: String /// The context value. diff --git a/Sources/AGUICore/Types/AgentExecution/RunAgentInput.swift b/Sources/AGUICore/Types/AgentExecution/RunAgentInput.swift index 39325d6..c1a8ac6 100644 --- a/Sources/AGUICore/Types/AgentExecution/RunAgentInput.swift +++ b/Sources/AGUICore/Types/AgentExecution/RunAgentInput.swift @@ -22,51 +22,6 @@ import Foundation /// - **context**: Additional contextual information /// - **forwardedProps**: Custom properties to forward to the agent /// -/// ## Usage Examples -/// -/// ```swift -/// // Simple agent execution -/// let input = RunAgentInput( -/// threadId: "thread-123", -/// runId: "run-456" -/// ) -/// -/// // With conversation history -/// let messages: [any Message] = [ -/// DeveloperMessage(id: "dev-1", content: "You are helpful"), -/// UserMessage(id: "user-1", content: "Hello!") -/// ] -/// -/// let input = RunAgentInput( -/// threadId: "thread-123", -/// runId: "run-456", -/// messages: messages -/// ) -/// -/// // With tools and context -/// let tools = [ -/// Tool(name: "get_weather", description: "Get weather", parameters: ...) -/// ] -/// -/// let contexts = [ -/// Context(description: "user_location", value: "San Francisco") -/// ] -/// -/// let input = RunAgentInput( -/// threadId: "thread-123", -/// runId: "run-456", -/// messages: messages, -/// tools: tools, -/// context: contexts -/// ) -/// ``` -/// -/// ## HTTP POST Request -/// -/// This type is typically serialized to JSON and sent as the body of -/// a POST request to an agent's endpoint. -/// -/// - SeeAlso: ``Message``, ``Tool``, ``Context`` public struct RunAgentInput: Sendable, Codable, Hashable { /// The conversation thread identifier. public let threadId: String diff --git a/Sources/AGUICore/Types/AgentExecution/RunAgentInputBuilder.swift b/Sources/AGUICore/Types/AgentExecution/RunAgentInputBuilder.swift index cd39d5b..90893b9 100644 --- a/Sources/AGUICore/Types/AgentExecution/RunAgentInputBuilder.swift +++ b/Sources/AGUICore/Types/AgentExecution/RunAgentInputBuilder.swift @@ -8,56 +8,6 @@ import Foundation /// instances through method chaining, making the code more readable and reducing /// errors when working with multiple optional parameters. /// -/// ## Basic Usage -/// -/// ```swift -/// let input = RunAgentInput.builder() -/// .threadId("thread-123") -/// .runId("run-456") -/// .build() -/// ``` -/// -/// ## Building Complex Inputs -/// -/// ```swift -/// let input = RunAgentInput.builder() -/// .threadId("chat-session-1") -/// .runId("run-1") -/// .message(DeveloperMessage(id: "dev-1", content: "You are helpful")) -/// .message(UserMessage(id: "user-1", content: "Hello!")) -/// .tool(weatherTool) -/// .contextItem(Context(description: "location", value: "SF")) -/// .build() -/// ``` -/// -/// ## Incremental Building -/// -/// ```swift -/// var builder = RunAgentInput.builder() -/// .threadId("thread-1") -/// .runId("run-1") -/// -/// // Add messages conditionally -/// if includeSystemPrompt { -/// builder = builder.message(SystemMessage(id: "sys-1", content: "Be concise")) -/// } -/// -/// let input = builder.build() -/// ``` -/// -/// ## Thread Safety -/// -/// The builder uses value semantics (struct) and is naturally thread-safe. Each -/// builder method returns a new builder instance, making it safe to use across -/// isolation boundaries and in concurrent contexts. -/// -/// ## Concurrency -/// -/// The builder uses value semantics and is intended for use on a single task or actor. -/// It does not conform to `Sendable` in Swift 6 because it contains mutable stored properties. -/// Prefer building inputs on one actor/task and then pass the resulting `RunAgentInput` across boundaries. -/// -/// - SeeAlso: `RunAgentInput` public struct RunAgentInputBuilder { private var _threadId: String? diff --git a/Sources/AGUICore/Types/AgentExecution/State.swift b/Sources/AGUICore/Types/AgentExecution/State.swift index 608d951..5c9ebd3 100644 --- a/Sources/AGUICore/Types/AgentExecution/State.swift +++ b/Sources/AGUICore/Types/AgentExecution/State.swift @@ -8,56 +8,4 @@ import Foundation /// information passed to and from agents during execution. The state can contain /// any JSON-serializable data structure including objects, arrays, primitives, and null. /// -/// ## Usage -/// -/// State is primarily used in ``RunAgentInput`` to provide contextual information -/// to agents. Applications can encode their state models as JSON and pass them -/// to agent endpoints. -/// -/// ```swift -/// // Create state from a Codable model -/// struct AppState: Codable { -/// let sessionId: String -/// let preferences: [String: String] -/// } -/// -/// let appState = AppState( -/// sessionId: "session-123", -/// preferences: ["theme": "dark"] -/// ) -/// let state: State = try JSONEncoder().encode(appState) -/// -/// // Or create state from raw JSON -/// let jsonState: State = Data(""" -/// { -/// "counter": 42, -/// "items": ["a", "b", "c"] -/// } -/// """.utf8) -/// ``` -/// -/// ## Default State -/// -/// An empty JSON object `{}` is typically used as the default state: -/// -/// ```swift -/// let emptyState: State = Data("{}".utf8) -/// ``` -/// -/// ## Type Design -/// -/// This is intentionally a simple type alias rather than a custom type to maintain -/// flexibility. Applications can: -/// - Encode any Codable type into State -/// - Decode State into any expected Codable type -/// - Manipulate State as raw Data when needed -/// - Pass State across actor boundaries (Data is Sendable) -/// -/// Future versions may introduce a more structured `JSONValue` enum if type safety -/// benefits outweigh flexibility concerns. -/// -/// - Note: State must contain valid JSON data. Invalid JSON will cause decoding -/// errors when processed by agents or client code. -/// -/// - SeeAlso: ``RunAgentInput`` public typealias State = Data diff --git a/Sources/AGUICore/Types/InputContent/AudioInputContent.swift b/Sources/AGUICore/Types/InputContent/AudioInputContent.swift index d764c2c..8f61c9f 100644 --- a/Sources/AGUICore/Types/InputContent/AudioInputContent.swift +++ b/Sources/AGUICore/Types/InputContent/AudioInputContent.swift @@ -8,7 +8,6 @@ import Foundation /// base64-encoded bytes. The optional `format` field identifies the audio codec /// (e.g., `"mp3"`, `"wav"`, `"ogg"`). /// -/// - SeeAlso: ``InputContent``, ``UserMessage`` public struct AudioInputContent: InputContent, Hashable, Sendable { /// The content type discriminator (always `"audio"`). diff --git a/Sources/AGUICore/Types/InputContent/BinaryInputContent.swift b/Sources/AGUICore/Types/InputContent/BinaryInputContent.swift index 8694a7f..0ce6d88 100644 --- a/Sources/AGUICore/Types/InputContent/BinaryInputContent.swift +++ b/Sources/AGUICore/Types/InputContent/BinaryInputContent.swift @@ -23,45 +23,6 @@ import Foundation /// - Audio: `"audio/mpeg"`, `"audio/wav"`, `"audio/ogg"` /// - Video: `"video/mp4"`, `"video/webm"` /// -/// ## Usage Examples -/// -/// ```swift -/// // Image from URL -/// let imageContent = BinaryInputContent( -/// mimeType: "image/jpeg", -/// url: "https://example.com/photo.jpg", -/// filename: "vacation.jpg" -/// ) -/// -/// // Embedded base64 image -/// let embeddedImage = BinaryInputContent( -/// mimeType: "image/png", -/// data: "iVBORw0KGgoAAAANSUhEUg...", -/// filename: "screenshot.png" -/// ) -/// -/// // Document by ID -/// let document = BinaryInputContent( -/// mimeType: "application/pdf", -/// id: "doc-annual-report-2024" -/// ) -/// -/// // Multimodal message -/// let contents: [any InputContent] = [ -/// TextInputContent(text: "Analyze this image and document:"), -/// imageContent, -/// document -/// ] -/// ``` -/// -/// ## Content Source Selection -/// -/// When multiple sources are provided, the consuming system typically prioritizes: -/// 1. **Data** (embedded): Fastest, no external fetch required -/// 2. **URL**: Enables streaming large files -/// 3. **ID**: Requires content management system integration -/// -/// - SeeAlso: ``InputContent``, ``TextInputContent``, ``UserMessage`` public struct BinaryInputContent: InputContent { /// Validation errors for BinaryInputContent public enum ValidationError: Error, LocalizedError { diff --git a/Sources/AGUICore/Types/InputContent/DocumentInputContent.swift b/Sources/AGUICore/Types/InputContent/DocumentInputContent.swift index 77919e7..9cfe98a 100644 --- a/Sources/AGUICore/Types/InputContent/DocumentInputContent.swift +++ b/Sources/AGUICore/Types/InputContent/DocumentInputContent.swift @@ -8,7 +8,6 @@ import Foundation /// base64-encoded bytes. The optional `mimeType` and `title` fields provide /// metadata about the document. /// -/// - SeeAlso: ``InputContent``, ``UserMessage`` public struct DocumentInputContent: InputContent, Hashable, Sendable { /// The content type discriminator (always `"document"`). diff --git a/Sources/AGUICore/Types/InputContent/ImageInputContent.swift b/Sources/AGUICore/Types/InputContent/ImageInputContent.swift index 0de7395..2215110 100644 --- a/Sources/AGUICore/Types/InputContent/ImageInputContent.swift +++ b/Sources/AGUICore/Types/InputContent/ImageInputContent.swift @@ -8,7 +8,6 @@ import Foundation /// base64-encoded bytes. The optional `detail` field controls how agents /// with vision capabilities process the image (e.g., `"high"`, `"low"`, `"auto"`). /// -/// - SeeAlso: ``InputContent``, ``UserMessage`` public struct ImageInputContent: InputContent, Hashable, Sendable { /// The content type discriminator (always `"image"`). diff --git a/Sources/AGUICore/Types/InputContent/InputContent.swift b/Sources/AGUICore/Types/InputContent/InputContent.swift index 2f9355d..02764c7 100644 --- a/Sources/AGUICore/Types/InputContent/InputContent.swift +++ b/Sources/AGUICore/Types/InputContent/InputContent.swift @@ -28,28 +28,6 @@ import Foundation /// - `"video"`: Deserializes to ``VideoInputContent`` /// - `"document"`: Deserializes to ``DocumentInputContent`` /// -/// ## Usage in UserMessage -/// -/// User messages can contain: -/// 1. **Simple text**: Single string content field -/// 2. **Multimodal**: Array of InputContent mixing text and binary data -/// -/// ```swift -/// // Multimodal message with text and image -/// let contents: [any InputContent] = [ -/// TextInputContent(text: "What's in this image?"), -/// BinaryInputContent( -/// mimeType: "image/png", -/// url: "https://example.com/photo.png" -/// ) -/// ] -/// ``` -/// -/// ## Type Discrimination -/// -/// The `type` property identifies the concrete content type during deserialization, -/// enabling the protocol to route JSON to the appropriate Swift type via DTOs. -/// /// ## Serialization /// /// InputContent types use the DTO pattern for serialization: diff --git a/Sources/AGUICore/Types/InputContent/TextInputContent.swift b/Sources/AGUICore/Types/InputContent/TextInputContent.swift index 20bb6f9..85825ec 100644 --- a/Sources/AGUICore/Types/InputContent/TextInputContent.swift +++ b/Sources/AGUICore/Types/InputContent/TextInputContent.swift @@ -8,36 +8,6 @@ import Foundation /// plain text. It is used alongside ``BinaryInputContent`` to build multimodal /// messages that combine text and binary data. /// -/// ## Usage -/// -/// Text content can be used in two ways: -/// -/// 1. **Simple text messages** (via UserMessage string content) -/// 2. **Multimodal messages** (via UserMessage content array) -/// -/// ```swift -/// // Standalone text content -/// let textContent = TextInputContent( -/// text: "What is the capital of France?" -/// ) -/// -/// // Mixed with binary content in multimodal message -/// let contents: [any InputContent] = [ -/// TextInputContent(text: "Analyze this image:"), -/// BinaryInputContent( -/// mimeType: "image/jpeg", -/// url: "https://example.com/photo.jpg" -/// ), -/// TextInputContent(text: "What objects do you see?") -/// ] -/// ``` -/// -/// ## Text Content Characteristics -/// -/// - **Simple**: Contains only text, no formatting or metadata -/// - **Flexible**: Supports any string content including Unicode, code, markdown -/// - **Composable**: Can be interleaved with binary content in multimodal messages -/// /// ## Type Discrimination /// /// The `type` field is always `"text"`, enabling polymorphic deserialization diff --git a/Sources/AGUICore/Types/InputContent/VideoInputContent.swift b/Sources/AGUICore/Types/InputContent/VideoInputContent.swift index 11cbfa2..8d0f9df 100644 --- a/Sources/AGUICore/Types/InputContent/VideoInputContent.swift +++ b/Sources/AGUICore/Types/InputContent/VideoInputContent.swift @@ -7,7 +7,6 @@ import Foundation /// `VideoInputContent` carries video data either as a URL reference or as /// base64-encoded bytes. /// -/// - SeeAlso: ``InputContent``, ``UserMessage`` public struct VideoInputContent: InputContent, Hashable, Sendable { /// The content type discriminator (always `"video"`). diff --git a/Sources/AGUICore/Types/Messages/ActivityMessage.swift b/Sources/AGUICore/Types/Messages/ActivityMessage.swift index 73c5fdd..e9c650a 100644 --- a/Sources/AGUICore/Types/Messages/ActivityMessage.swift +++ b/Sources/AGUICore/Types/Messages/ActivityMessage.swift @@ -21,68 +21,6 @@ import Foundation /// The `content` field stores flexible JSON data as a `Data` object, /// allowing each activity type to define its own content structure. /// -/// ## Usage Examples -/// -/// ```swift -/// // Progress indicator -/// let progressContent = Data(""" -/// { -/// "percent": 75, -/// "message": "Processing files...", -/// "current": 15, -/// "total": 20 -/// } -/// """.utf8) -/// -/// let progress = ActivityMessage( -/// id: "progress-1", -/// activityType: "progress", -/// content: progressContent -/// ) -/// -/// // Chart visualization -/// let chartContent = Data(""" -/// { -/// "chartType": "bar", -/// "data": { -/// "labels": ["Q1", "Q2", "Q3", "Q4"], -/// "datasets": [ -/// {"label": "Sales", "values": [100, 150, 120, 180]} -/// ] -/// } -/// } -/// """.utf8) -/// -/// let chart = ActivityMessage( -/// id: "viz-1", -/// activityType: "chart", -/// content: chartContent -/// ) -/// -/// // A2UI form surface -/// let formContent = Data(""" -/// { -/// "surfaceType": "form", -/// "fields": [ -/// {"name": "email", "type": "text"}, -/// {"name": "submit", "type": "button"} -/// ] -/// } -/// """.utf8) -/// -/// let form = ActivityMessage( -/// id: "surface-1", -/// activityType: "a2ui-form", -/// content: formContent -/// ) -/// ``` -/// -/// ## Message Protocol -/// -/// ActivityMessage conforms to the Message protocol. `name` and `encryptedValue` -/// are always `nil` since activities use structured JSON `content` instead. -/// -/// - SeeAlso: ``Message``, ``Role`` public struct ActivityMessage: Message, Sendable, Hashable { /// The unique identifier for this message. public let id: String diff --git a/Sources/AGUICore/Types/Messages/AssistantMessage.swift b/Sources/AGUICore/Types/Messages/AssistantMessage.swift index f3873f0..dd67721 100644 --- a/Sources/AGUICore/Types/Messages/AssistantMessage.swift +++ b/Sources/AGUICore/Types/Messages/AssistantMessage.swift @@ -24,51 +24,6 @@ import Foundation /// 3. Add tool calls through ToolCallStart/Args/End events /// 4. Complete with TextMessageEnd or ToolCallEnd events /// -/// ## Example -/// -/// ```swift -/// // Simple text response -/// let textResponse = AssistantMessage( -/// id: "asst-1", -/// content: "I understand your question. Let me explain..." -/// ) -/// -/// // Tool call with explanation -/// let weatherCall = ToolCall( -/// id: "call_weather", -/// function: FunctionCall( -/// name: "get_current_weather", -/// arguments: "{\"location\":\"San Francisco\"}" -/// ) -/// ) -/// -/// let toolResponse = AssistantMessage( -/// id: "asst-2", -/// content: "Let me check the weather for you.", -/// toolCalls: [weatherCall] -/// ) -/// -/// // Multiple simultaneous tool calls -/// let multiToolResponse = AssistantMessage( -/// id: "asst-3", -/// content: "Gathering information from multiple sources...", -/// toolCalls: [ -/// ToolCall(...), -/// ToolCall(...), -/// ToolCall(...) -/// ] -/// ) -/// ``` -/// -/// ## Tool Call Flow -/// -/// When an assistant message includes tool calls: -/// 1. Assistant creates message with toolCalls array -/// 2. Tool system executes each tool call -/// 3. Results returned in ``ToolMessage`` instances with matching toolCallId -/// 4. Assistant processes results and continues conversation -/// -/// - SeeAlso: ``Message``, ``ToolCall``, ``ToolMessage`` public struct AssistantMessage: Message, Sendable, Hashable { /// Unique identifier for this message. public let id: String diff --git a/Sources/AGUICore/Types/Messages/DeveloperMessage.swift b/Sources/AGUICore/Types/Messages/DeveloperMessage.swift index 2973761..2ab4e55 100644 --- a/Sources/AGUICore/Types/Messages/DeveloperMessage.swift +++ b/Sources/AGUICore/Types/Messages/DeveloperMessage.swift @@ -9,40 +9,6 @@ import Foundation /// typically contain meta-instructions about how the agent should behave or /// technical configuration details. /// -/// ## Use Cases -/// -/// Developer messages are used for: -/// - System configuration and initialization -/// - Meta-instructions about agent behavior -/// - Technical constraints and requirements -/// - Administrative control messages -/// - Debug and logging configuration -/// -/// ## Example -/// -/// ```swift -/// let configMessage = DeveloperMessage( -/// id: "dev-config-1", -/// content: """ -/// System configuration: -/// - Enable debug logging -/// - Set max response length to 2000 tokens -/// - Use conservative safety settings -/// """, -/// name: "SystemConfigurator" -/// ) -/// ``` -/// -/// ## Differences from SystemMessage -/// -/// While both developer and system messages guide agent behavior: -/// - **DeveloperMessage**: System-level technical configuration and meta-instructions -/// - **SystemMessage**: High-level behavioral guidelines and personality instructions -/// -/// Developer messages typically contain more technical, configuration-oriented -/// content, while system messages focus on behavioral patterns and response style. -/// -/// - SeeAlso: ``Message``, ``SystemMessage`` public struct DeveloperMessage: Message, Sendable, Hashable { /// Unique identifier for this message. public let id: String diff --git a/Sources/AGUICore/Types/Messages/Message.swift b/Sources/AGUICore/Types/Messages/Message.swift index 4fabf7d..e066574 100644 --- a/Sources/AGUICore/Types/Messages/Message.swift +++ b/Sources/AGUICore/Types/Messages/Message.swift @@ -37,25 +37,6 @@ import Foundation /// All message types must conform to `Sendable` to safely cross actor isolation /// boundaries in Swift's structured concurrency model. /// -/// ## Example -/// -/// ```swift -/// // Creating different message types -/// let systemMsg = SystemMessage( -/// id: "msg-1", -/// content: "You are a helpful assistant." -/// ) -/// -/// let userMsg = UserMessage( -/// id: "msg-2", -/// content: "Hello, how can you help me?" -/// ) -/// -/// // All messages conform to the same protocol -/// let messages: [any Message] = [systemMsg, userMsg] -/// ``` -/// -/// - SeeAlso: ``Role`` public protocol Message: Sendable { /// Unique identifier for this message. /// diff --git a/Sources/AGUICore/Types/Messages/ReasoningMessage.swift b/Sources/AGUICore/Types/Messages/ReasoningMessage.swift index 8d158f0..4d6fd3d 100644 --- a/Sources/AGUICore/Types/Messages/ReasoningMessage.swift +++ b/Sources/AGUICore/Types/Messages/ReasoningMessage.swift @@ -19,23 +19,6 @@ import Foundation /// by the agent's reasoning process, delivered via ``ReasoningEncryptedValueEvent``. /// It can be used in verified reasoning workflows to authenticate the reasoning output. /// -/// ## Example -/// -/// ```swift -/// let reasoning = ReasoningMessage( -/// id: "reasoning-1", -/// content: "Let me think step by step about this problem..." -/// ) -/// -/// // With an encrypted value for verified reasoning -/// let verifiedReasoning = ReasoningMessage( -/// id: "reasoning-2", -/// content: "First, I need to analyse the inputs...", -/// encryptedValue: "" -/// ) -/// ``` -/// -/// - SeeAlso: ``Message``, ``Role``, ``ReasoningStartEvent``, ``ReasoningEncryptedValueEvent`` public struct ReasoningMessage: Message, Sendable, Hashable { /// The unique identifier for this message. public let id: String diff --git a/Sources/AGUICore/Types/Messages/Role.swift b/Sources/AGUICore/Types/Messages/Role.swift index 4c67cec..b12c63d 100644 --- a/Sources/AGUICore/Types/Messages/Role.swift +++ b/Sources/AGUICore/Types/Messages/Role.swift @@ -10,7 +10,6 @@ import Foundation /// The role field serves as the discriminator for polymorphic message types, /// allowing the protocol to deserialize messages into their specific concrete types. /// -/// - SeeAlso: ``Message`` public enum Role: String, Sendable, Codable, CaseIterable, Hashable { /// Developer-level instructions and configuration messages. /// diff --git a/Sources/AGUICore/Types/Messages/SystemMessage.swift b/Sources/AGUICore/Types/Messages/SystemMessage.swift index cd84ce2..8ffe3ff 100644 --- a/Sources/AGUICore/Types/Messages/SystemMessage.swift +++ b/Sources/AGUICore/Types/Messages/SystemMessage.swift @@ -18,41 +18,6 @@ import Foundation /// - Establishing response format preferences /// - Configuring safety and ethical boundaries /// -/// ## Example -/// -/// ```swift -/// let systemPrompt = SystemMessage( -/// id: "sys-1", -/// content: """ -/// You are a professional coding assistant with expertise in Swift. -/// Always: -/// - Explain your reasoning -/// - Write clean, well-documented code -/// - Follow Swift best practices -/// - Be concise but thorough -/// """, -/// name: "SwiftExpert" -/// ) -/// ``` -/// -/// ## Optional Content -/// -/// Unlike ``DeveloperMessage``, system messages allow nil content, which can be -/// useful for: -/// - Placeholder system messages to be filled later -/// - System messages that rely only on name for identification -/// - Resetting or clearing system context -/// -/// ## Differences from DeveloperMessage -/// -/// While both guide agent behavior: -/// - **SystemMessage**: High-level behavioral guidelines, personality, and response patterns -/// - **DeveloperMessage**: System-level technical configuration and meta-instructions -/// -/// System messages focus on how the agent should communicate and behave, while -/// developer messages focus on technical constraints and system configuration. -/// -/// - SeeAlso: ``Message``, ``DeveloperMessage`` public struct SystemMessage: Message, Sendable, Hashable { /// Unique identifier for this message. public let id: String diff --git a/Sources/AGUICore/Types/Messages/ToolMessage.swift b/Sources/AGUICore/Types/Messages/ToolMessage.swift index e1329e3..b4cec5a 100644 --- a/Sources/AGUICore/Types/Messages/ToolMessage.swift +++ b/Sources/AGUICore/Types/Messages/ToolMessage.swift @@ -18,42 +18,6 @@ import Foundation /// - Delivering API responses to the agent /// - Communicating database query results /// -/// ## Example -/// -/// ```swift -/// // Successful tool execution -/// let successMessage = ToolMessage( -/// id: "tool-msg-1", -/// content: "Successfully saved 3 files to /documents", -/// toolCallId: "call-save-123", -/// name: "save_files" -/// ) -/// -/// // Failed tool execution -/// let errorMessage = ToolMessage( -/// id: "tool-msg-2", -/// content: "Operation failed", -/// toolCallId: "call-delete-456", -/// name: "delete_file", -/// error: "Permission denied: Cannot delete system file" -/// ) -/// ``` -/// -/// ## Tool Call Linkage -/// -/// The ``toolCallId`` property is critical for maintaining the request-response flow: -/// 1. Assistant sends a tool call with ID "call-123" -/// 2. Tool executes and returns a ToolMessage with toolCallId = "call-123" -/// 3. Agent correlates the result with the original request -/// -/// ## Error Handling -/// -/// When tool execution fails, use the ``error`` property to communicate the failure: -/// - Set ``content`` to a user-friendly error description -/// - Set ``error`` to a technical error message for debugging -/// - The agent can then decide how to handle or report the error -/// -/// - SeeAlso: ``Message``, ``AssistantMessage`` public struct ToolMessage: Message, Sendable, Hashable { /// Unique identifier for this message. public let id: String diff --git a/Sources/AGUICore/Types/Messages/UserMessage.swift b/Sources/AGUICore/Types/Messages/UserMessage.swift index 04b05a6..bd0fc56 100644 --- a/Sources/AGUICore/Types/Messages/UserMessage.swift +++ b/Sources/AGUICore/Types/Messages/UserMessage.swift @@ -7,45 +7,6 @@ import Foundation /// `UserMessage` supports both simple text messages and multimodal messages /// containing text, images, audio, documents, and other binary data. /// -/// ## Text-only Messages -/// -/// For simple text input, use the standard initializer: -/// -/// ```swift -/// let message = UserMessage( -/// id: "user-1", -/// content: "What is the weather like today?" -/// ) -/// ``` -/// -/// ## Multimodal Messages -/// -/// For rich input combining text and binary data, use the multimodal factory: -/// -/// ```swift -/// let parts: [any InputContent] = [ -/// TextInputContent(text: "What's in this image?"), -/// BinaryInputContent( -/// mimeType: "image/jpeg", -/// url: "https://example.com/photo.jpg" -/// ) -/// ] -/// -/// let message = UserMessage.multimodal( -/// id: "user-2", -/// parts: parts -/// ) -/// ``` -/// -/// ## Serialization -/// -/// UserMessage uses custom Codable serialization: -/// - **Text-only**: `content` field is a JSON string -/// - **Multimodal**: `content` field is a JSON array of InputContent objects -/// -/// The serialization is transparent and handled automatically during encoding/decoding. -/// -/// - SeeAlso: ``Message``, ``InputContent``, ``TextInputContent``, ``BinaryInputContent`` public struct UserMessage: Message, Sendable, Hashable { /// The unique identifier for this message. public let id: String diff --git a/Sources/AGUICore/Types/Tools/FunctionCall.swift b/Sources/AGUICore/Types/Tools/FunctionCall.swift index ccc237b..92a5293 100644 --- a/Sources/AGUICore/Types/Tools/FunctionCall.swift +++ b/Sources/AGUICore/Types/Tools/FunctionCall.swift @@ -17,43 +17,6 @@ import Foundation /// - Supports dynamic function signatures /// - Maintains compatibility with various serialization formats /// -/// ## Example -/// -/// ```swift -/// // Simple function call with basic arguments -/// let weatherCall = FunctionCall( -/// name: "get_weather", -/// arguments: """ -/// { -/// "location": "San Francisco", -/// "units": "celsius" -/// } -/// """ -/// ) -/// -/// // Function with no arguments -/// let pingCall = FunctionCall( -/// name: "ping", -/// arguments: "{}" -/// ) -/// -/// // Parsing arguments at execution time -/// struct WeatherArgs: Codable { -/// let location: String -/// let units: String -/// } -/// -/// let argsData = Data(weatherCall.arguments.utf8) -/// let parsedArgs = try JSONDecoder().decode(WeatherArgs.self, from: argsData) -/// ``` -/// -/// ## Relationship with ToolCall -/// -/// `FunctionCall` is typically embedded within a ``ToolCall``, which adds: -/// - A unique identifier for tracking the call -/// - The function type discriminator -/// -/// - SeeAlso: ``ToolCall``, ``Tool`` public struct FunctionCall: Sendable, Codable, Hashable { /// The name of the function to invoke. /// diff --git a/Sources/AGUICore/Types/Tools/Tool.swift b/Sources/AGUICore/Types/Tools/Tool.swift index 2781559..e6793b0 100644 --- a/Sources/AGUICore/Types/Tools/Tool.swift +++ b/Sources/AGUICore/Types/Tools/Tool.swift @@ -10,79 +10,6 @@ import Foundation /// - Ask for human input or confirmation /// - Access specialized capabilities beyond the agent's core knowledge /// -/// ## Tool Definition -/// -/// Each tool is defined by: -/// - **Name**: A unique identifier for the tool (e.g., "get_weather", "send_email") -/// - **Description**: Human-readable explanation of what the tool does -/// - **Parameters**: JSON Schema defining the expected input structure -/// -/// ## JSON Schema Parameters -/// -/// The parameters field contains a JSON Schema that defines: -/// - Required and optional parameters -/// - Parameter types (string, integer, boolean, object, array) -/// - Validation rules (enums, min/max values, patterns) -/// - Default values -/// - Parameter descriptions for agent understanding -/// -/// ## Example -/// -/// ```swift -/// // Define a weather tool -/// let weatherSchema = Data(""" -/// { -/// "type": "object", -/// "properties": { -/// "location": { -/// "type": "string", -/// "description": "City and state, e.g., San Francisco, CA" -/// }, -/// "unit": { -/// "type": "string", -/// "enum": ["celsius", "fahrenheit"], -/// "default": "fahrenheit" -/// } -/// }, -/// "required": ["location"] -/// } -/// """.utf8) -/// -/// let weatherTool = Tool( -/// name: "get_current_weather", -/// description: "Get the current weather in a given location", -/// parameters: weatherSchema -/// ) -/// -/// // Register tools with the agent -/// let tools = [weatherTool] -/// ``` -/// -/// ## Tool Usage Flow -/// -/// 1. **Registration**: Tools are registered with the agent system -/// 2. **Selection**: Agent analyzes user request and selects appropriate tool -/// 3. **Invocation**: Agent creates a ``ToolCall`` with function arguments -/// 4. **Execution**: Tool system validates arguments against schema and executes -/// 5. **Response**: Results returned in a ``ToolMessage`` -/// -/// ## Schema Validation -/// -/// The JSON Schema in the parameters field enables: -/// - Automatic argument validation before execution -/// - Type safety for tool implementations -/// - Clear documentation for agents about expected inputs -/// - IDE support and autocomplete for tool arguments -/// -/// ## Design Considerations -/// -/// Parameters are stored as `Data` (raw JSON Schema) rather than a parsed structure to: -/// - Maintain flexibility in schema complexity -/// - Defer validation to execution time -/// - Support evolving JSON Schema standards -/// - Enable custom schema extensions -/// -/// - SeeAlso: ``ToolCall``, ``FunctionCall``, ``ToolMessage`` public struct Tool: Sendable, Codable, Hashable { /// The unique identifier for this tool. /// diff --git a/Sources/AGUICore/Types/Tools/ToolCall.swift b/Sources/AGUICore/Types/Tools/ToolCall.swift index 97809f5..3816b25 100644 --- a/Sources/AGUICore/Types/Tools/ToolCall.swift +++ b/Sources/AGUICore/Types/Tools/ToolCall.swift @@ -15,50 +15,6 @@ import Foundation /// 3. **Response**: Results are returned in a ``ToolMessage`` with matching toolCallId /// 4. **Correlation**: Agent matches response to request using the ID /// -/// ## Type Field -/// -/// The `type` field is always `"function"` and is included in JSON serialization -/// to maintain protocol compatibility and avoid conflicts with event discriminators. -/// -/// ## Example -/// -/// ```swift -/// // Create a tool call -/// let weatherCall = ToolCall( -/// id: "call_weather_123", -/// function: FunctionCall( -/// name: "get_current_weather", -/// arguments: """ -/// { -/// "location": "San Francisco", -/// "unit": "celsius" -/// } -/// """ -/// ) -/// ) -/// -/// // Later, receive the response -/// let response = ToolMessage( -/// id: "msg_1", -/// content: "Temperature: 18°C, Conditions: Partly cloudy", -/// toolCallId: weatherCall.id // Links back to the call -/// ) -/// ``` -/// -/// ## Multiple Tool Calls -/// -/// Agents can request multiple tool executions simultaneously by creating -/// an array of ToolCalls, each with a unique ID: -/// -/// ```swift -/// let toolCalls: [ToolCall] = [ -/// ToolCall(id: "call_1", function: FunctionCall(...)), -/// ToolCall(id: "call_2", function: FunctionCall(...)), -/// ToolCall(id: "call_3", function: FunctionCall(...)) -/// ] -/// ``` -/// -/// - SeeAlso: ``FunctionCall``, ``ToolMessage``, ``Tool`` public struct ToolCall: Sendable, Codable, Hashable { /// Unique identifier for this tool call. /// diff --git a/Sources/AGUICore/Utilities/JSONCodingHelpers.swift b/Sources/AGUICore/Utilities/JSONCodingHelpers.swift index 121ec9c..0feeb93 100644 --- a/Sources/AGUICore/Utilities/JSONCodingHelpers.swift +++ b/Sources/AGUICore/Utilities/JSONCodingHelpers.swift @@ -8,15 +8,6 @@ import Foundation /// at compile time, commonly needed when bridging between strongly-typed Swift /// and loosely-typed JSON. /// -/// ## Usage -/// -/// Use with `KeyedEncodingContainer` and `KeyedDecodingContainer` to handle -/// arbitrary JSON structures: -/// -/// ```swift -/// let container = try decoder.container(keyedBy: JSONCodingKeys.self) -/// let jsonObject = try container.decodeJSONObject() -/// ``` public struct JSONCodingKeys: CodingKey { public var stringValue: String public var intValue: Int? diff --git a/Sources/AGUITools/Core/ToolExecutionContext.swift b/Sources/AGUITools/Core/ToolExecutionContext.swift index 6c5fd4e..f2fa2ce 100644 --- a/Sources/AGUITools/Core/ToolExecutionContext.swift +++ b/Sources/AGUITools/Core/ToolExecutionContext.swift @@ -9,24 +9,6 @@ import Foundation /// including the tool call being executed, optional thread and run identifiers for tracking, /// and metadata for additional execution context. /// -/// ## Usage -/// -/// ```swift -/// // Minimal context -/// let context = ToolExecutionContext(toolCall: toolCall) -/// -/// // Full context with tracking and metadata -/// let context = ToolExecutionContext( -/// toolCall: toolCall, -/// threadId: "thread_abc123", -/// runId: "run_xyz789", -/// metadata: [ -/// "userId": "user_123", -/// "sessionId": "session_456", -/// "timestamp": "2025-01-01T12:00:00Z" -/// ] -/// ) -/// ``` /// /// ## Thread and Run IDs /// diff --git a/Sources/AGUITools/Core/ToolExecutionManager.swift b/Sources/AGUITools/Core/ToolExecutionManager.swift index 5040cc7..08b5be1 100644 --- a/Sources/AGUITools/Core/ToolExecutionManager.swift +++ b/Sources/AGUITools/Core/ToolExecutionManager.swift @@ -13,25 +13,6 @@ import Foundation /// - Tool execution happens concurrently with event forwarding. /// - Execution lifecycle notifications are published via `executionEvents`. /// -/// ## Usage -/// -/// ```swift -/// let manager = ToolExecutionManager( -/// toolRegistry: registry, -/// responseHandler: myHandler -/// ) -/// -/// let processedStream = manager.processEventStream( -/// rawStream, -/// threadId: "thread_123", -/// runId: "run_456" -/// ) -/// -/// for try await event in processedStream { -/// // Handle events as usual — tool calls are executed automatically -/// } -/// ``` -/// /// - SeeAlso: ``ToolRegistry``, ``ToolResponseHandler``, ``ToolExecutionEvent`` public actor ToolExecutionManager { diff --git a/Sources/AGUITools/Core/ToolExecutionResult.swift b/Sources/AGUITools/Core/ToolExecutionResult.swift index 4693f07..a969de3 100644 --- a/Sources/AGUITools/Core/ToolExecutionResult.swift +++ b/Sources/AGUITools/Core/ToolExecutionResult.swift @@ -9,22 +9,6 @@ import Foundation /// This type supports both successful outcomes with data and failed outcomes with /// error information. /// -/// ## Usage -/// -/// ```swift -/// // Successful execution with data -/// let jsonData = Data(#"{"temperature": 72, "conditions": "sunny"}"#.utf8) -/// let result = ToolExecutionResult.success( -/// result: jsonData, -/// message: "Weather retrieved successfully" -/// ) -/// -/// // Failed execution -/// let error = ToolExecutionResult.failure( -/// message: "Failed to connect to weather service" -/// ) -/// ``` -/// /// ## Design Notes /// /// - Result data is stored as `Data` (JSON) to maintain flexibility diff --git a/Sources/AGUITools/Core/ToolExecutor.swift b/Sources/AGUITools/Core/ToolExecutor.swift index 65a4f7a..aa2983b 100644 --- a/Sources/AGUITools/Core/ToolExecutor.swift +++ b/Sources/AGUITools/Core/ToolExecutor.swift @@ -17,41 +17,6 @@ import Foundation /// - Defensive (validate all inputs) /// - Fast (avoid blocking operations when possible) /// -/// ## Usage -/// -/// ```swift -/// actor WeatherToolExecutor: ToolExecutor { -/// let tool: Tool -/// -/// init() { -/// self.tool = Tool( -/// name: "get_weather", -/// description: "Get current weather for a location", -/// parameters: Data(#"{"type": "object", ...}"#.utf8) -/// ) -/// } -/// -/// func execute(context: ToolExecutionContext) async throws -> ToolExecutionResult { -/// // Decode arguments -/// let args = try JSONDecoder().decode( -/// WeatherArgs.self, -/// from: Data(context.toolCall.function.arguments.utf8) -/// ) -/// -/// // Execute the tool -/// let weather = try await fetchWeather(for: args.location) -/// -/// // Return result -/// let resultData = try JSONEncoder().encode(weather) -/// return .success(result: resultData, message: "Weather retrieved") -/// } -/// -/// func maximumExecutionTime() -> Duration? { -/// .seconds(30) -/// } -/// } -/// ``` -/// /// ## Thread Safety /// /// Tool executors should be implemented as actors to ensure thread-safe @@ -123,18 +88,6 @@ public extension ToolExecutor { /// Indicates whether a tool call's arguments are valid according to the /// tool's parameter schema, and provides error messages for invalid calls. /// -/// ## Usage -/// -/// ```swift -/// // Valid tool call -/// let result = ToolValidationResult.valid -/// -/// // Invalid tool call with errors -/// let result = ToolValidationResult.invalid(errors: [ -/// "Missing required parameter: location", -/// "Invalid type for parameter 'temperature': expected number" -/// ]) -/// ``` public struct ToolValidationResult: Sendable { /// Whether the validation passed. public let isValid: Bool @@ -186,30 +139,6 @@ public struct ToolValidationResult: Sendable { /// - **toolNotFound**: Requested tool doesn't exist (configuration error, not retryable) /// - **executionFailed**: General execution failure (may be retryable depending on cause) /// -/// ## Usage -/// -/// ```swift -/// // Validation failure -/// throw ToolExecutionError.validationFailed( -/// message: "Missing required parameter: location" -/// ) -/// -/// // Timeout -/// throw ToolExecutionError.timeout( -/// toolName: "slow_api_call", -/// duration: .seconds(30) -/// ) -/// -/// // Execution failure with underlying error -/// do { -/// try await apiCall() -/// } catch { -/// throw ToolExecutionError.executionFailed( -/// toolName: "api_tool", -/// underlyingError: error -/// ) -/// } -/// ``` public enum ToolExecutionError: Error, Sendable { /// Tool call validation failed. /// diff --git a/Sources/AGUITools/Registry/ToolExecutionStats.swift b/Sources/AGUITools/Registry/ToolExecutionStats.swift index b946217..21f14ef 100644 --- a/Sources/AGUITools/Registry/ToolExecutionStats.swift +++ b/Sources/AGUITools/Registry/ToolExecutionStats.swift @@ -9,15 +9,6 @@ import Foundation /// are useful for monitoring tool performance, identifying problematic tools, /// and debugging execution issues. /// -/// ## Usage -/// -/// ```swift -/// // Get stats from a registry -/// let stats = await registry.stats(for: "my_tool") -/// print("Success rate: \(stats.successRate * 100)%") -/// print("Average time: \(stats.averageExecutionTime)") -/// ``` -/// /// ## Metrics Tracked /// /// - **Execution counts**: Total, successful, and failed executions @@ -30,7 +21,6 @@ import Foundation /// - Sendable for safe concurrent access /// - Success rate computed property for convenience /// -/// - SeeAlso: ``ToolRegistry``, ``DefaultToolRegistry`` public struct ToolExecutionStats: Sendable, Equatable, Hashable { /// Total number of executions (successes + failures). public let executionCount: Int @@ -74,11 +64,6 @@ public struct ToolExecutionStats: Sendable, Equatable, Hashable { /// Calculated as `successCount / executionCount`. Returns 0.0 if there /// have been no executions. /// - /// ## Examples - /// - /// - 10 successes out of 10 executions = 1.0 (100%) - /// - 7 successes out of 10 executions = 0.7 (70%) - /// - 0 executions = 0.0 public var successRate: Double { guard executionCount > 0 else { return 0.0 } return Double(successCount) / Double(executionCount) diff --git a/Sources/AGUITools/Registry/ToolRegistry.swift b/Sources/AGUITools/Registry/ToolRegistry.swift index 628c839..f64be8e 100644 --- a/Sources/AGUITools/Registry/ToolRegistry.swift +++ b/Sources/AGUITools/Registry/ToolRegistry.swift @@ -11,24 +11,6 @@ import Foundation /// - Tracking execution statistics /// - Managing tool lifecycle /// -/// ## Usage -/// -/// ```swift -/// // Create and configure a registry -/// let registry = DefaultToolRegistry() -/// -/// // Register tools -/// try await registry.register(executor: MyToolExecutor()) -/// -/// // Execute a tool call -/// let result = try await registry.execute(context: context) -/// -/// // Query statistics -/// if let stats = await registry.stats(for: "my_tool") { -/// print("Success rate: \(stats.successRate)") -/// } -/// ``` -/// /// ## Thread Safety /// /// All ToolRegistry implementations must be thread-safe and support @@ -127,24 +109,6 @@ public enum ToolRegistryError: Error, Sendable { /// - Supports timeout handling based on tool configuration /// - Handles errors gracefully with statistics updates /// -/// ## Usage -/// -/// ```swift -/// // Create a registry -/// let registry = DefaultToolRegistry() -/// -/// // Register tools -/// try await registry.register(executor: WeatherToolExecutor()) -/// try await registry.register(executor: CalculatorToolExecutor()) -/// -/// // Execute tool calls from agent -/// for toolCall in agentToolCalls { -/// let context = ToolExecutionContext(toolCall: toolCall) -/// let result = try await registry.execute(context: context) -/// // Send result back to agent -/// } -/// ``` -/// /// ## Thread Safety /// /// This actor-based implementation provides automatic thread safety through diff --git a/Tests/AGUIClientTests/Transport/MockHTTPClient.swift b/Tests/AGUIClientTests/Transport/MockHTTPClient.swift index 04358a9..8050aec 100644 --- a/Tests/AGUIClientTests/Transport/MockHTTPClient.swift +++ b/Tests/AGUIClientTests/Transport/MockHTTPClient.swift @@ -234,27 +234,6 @@ extension Dictionary where Key == UUID { /// This protocol intercepts URLSession requests and returns mock data from a /// thread-safe registry, enabling isolated testing without network calls. /// -/// ## Usage -/// -/// Don't instantiate this directly. Use `URLSession.makeMockSession(registry:)` -/// to create a properly configured test session. -/// -/// ## Example -/// -/// ```swift -/// let registry = MockURLProtocolRegistry() -/// await registry.register( -/// url: URL(string: "https://api.example.com/data")!, -/// data: Data("response".utf8), -/// statusCode: 200 -/// ) -/// -/// let session = URLSession.makeMockSession(registry: registry) -/// let client = URLSessionHTTPClient(session: session) -/// -/// // Requests will use mocked responses from registry -/// let (data, _) = try await session.data(from: URL(string: "https://api.example.com/data")!) -/// ``` final class MockURLProtocol: URLProtocol { /// Header key for passing the registry ID static let registryIDHeader = "X-MockURLProtocol-Registry-ID" diff --git a/Tests/AGUICoreTests/Helpers/AGUIEventDecoderTestHelpers.swift b/Tests/AGUICoreTests/Helpers/AGUIEventDecoderTestHelpers.swift index 43283bb..4b593ac 100644 --- a/Tests/AGUICoreTests/Helpers/AGUIEventDecoderTestHelpers.swift +++ b/Tests/AGUICoreTests/Helpers/AGUIEventDecoderTestHelpers.swift @@ -8,19 +8,6 @@ import XCTest /// Conforming test classes automatically gain access to standardized /// decoder factory methods, ensuring consistency across all event tests. /// -/// ## Usage -/// -/// Add this protocol to your test class: -/// ```swift -/// final class MyEventTests: XCTestCase, AGUIEventDecoderTestHelpers { -/// func test_decodeEvent() { -/// let decoder = makeStrictDecoder() -/// let data = jsonData("""{"type": "MY_EVENT"}""") -/// // ... -/// } -/// } -/// ``` -/// /// ## Benefits /// /// - **Single Source of Truth**: All decoder configuration in one place diff --git a/Tests/AGUICoreTests/Helpers/EventDecodingErrorTests.swift b/Tests/AGUICoreTests/Helpers/EventDecodingErrorTests.swift index b89d7e1..685df10 100644 --- a/Tests/AGUICoreTests/Helpers/EventDecodingErrorTests.swift +++ b/Tests/AGUICoreTests/Helpers/EventDecodingErrorTests.swift @@ -8,24 +8,6 @@ import XCTest /// Event test classes conforming to this protocol must implement required properties /// describing their event type, then automatically inherit 6 standard error tests. /// -/// ## Usage -/// -/// ```swift -/// final class RunStartedEventTests: XCTestCase, -/// AGUIEventDecoderTestHelpers, -/// EventDecodingErrorTests { -/// -/// var validEventFieldsWithoutType: [String: Any] { -/// ["threadId": EventTestData.threadId, "runId": EventTestData.runId] -/// } -/// -/// var eventTypeString: String { "RUN_STARTED" } -/// var expectedEventType: EventType { .runStarted } -/// -/// // 6 error tests are automatically inherited! -/// } -/// ``` -/// /// ## Benefits /// /// - **Eliminates Duplication**: Removes 78+ duplicated error tests across 13 files From d55ecccb78c5b360b46a71f8030c4b7271227a4f Mon Sep 17 00:00:00 2001 From: paduh Date: Sun, 3 May 2026 00:12:12 -0400 Subject: [PATCH 2/2] implement circuit breaker --- Sources/AGUITools/Core/ToolErrorHandler.swift | 13 + Sources/AGUITools/Core/ToolExecutor.swift | 10 + Sources/AGUITools/Registry/ToolRegistry.swift | 94 +++-- .../Core/ToolErrorHandlerTests.swift | 326 ++++++++++++++++++ .../CircuitBreakerIntegrationTests.swift | 257 ++++++++++++++ 5 files changed, 680 insertions(+), 20 deletions(-) create mode 100644 Tests/AGUIToolsTests/Core/ToolErrorHandlerTests.swift create mode 100644 Tests/AGUIToolsTests/Registry/CircuitBreakerIntegrationTests.swift diff --git a/Sources/AGUITools/Core/ToolErrorHandler.swift b/Sources/AGUITools/Core/ToolErrorHandler.swift index 289c1b3..3bc4ad8 100644 --- a/Sources/AGUITools/Core/ToolErrorHandler.swift +++ b/Sources/AGUITools/Core/ToolErrorHandler.swift @@ -313,6 +313,17 @@ public actor ToolErrorHandler { await circuitBreaker.recordSuccess() } + /// Returns `true` if the circuit allows the next execution attempt. + /// + /// Call this **before** attempting tool execution to implement fast-fail + /// semantics. When the circuit is open, returns `false` immediately without + /// touching the underlying executor. After the recovery timeout elapses, + /// `allowRequest()` transitions the circuit to ``CircuitBreakerState/halfOpen`` + /// and returns `true`, allowing one probe execution through. + public func shouldAllowExecution() async -> Bool { + await circuitBreaker.allowRequest() + } + /// Returns the current circuit breaker state. public func circuitBreakerState() async -> CircuitBreakerState { await circuitBreaker.currentState() @@ -348,6 +359,8 @@ public actor ToolErrorHandler { return config.retryOnNotFound case .timeout, .executionFailed: return true + case .circuitBreakerOpen: + return false } } diff --git a/Sources/AGUITools/Core/ToolExecutor.swift b/Sources/AGUITools/Core/ToolExecutor.swift index aa2983b..c6de6f9 100644 --- a/Sources/AGUITools/Core/ToolExecutor.swift +++ b/Sources/AGUITools/Core/ToolExecutor.swift @@ -175,4 +175,14 @@ public enum ToolExecutionError: Error, Sendable { /// - toolName: Name of the tool that failed /// - underlyingError: The underlying error that caused the failure case executionFailed(toolName: String, underlyingError: Error) + + /// Execution rejected because the tool's circuit breaker is open. + /// + /// The tool has exceeded its failure threshold and all calls are being + /// fast-failed until the recovery timeout elapses and the probe succeeds. + /// Check ``DefaultToolRegistry/circuitBreakerState(for:)`` to inspect + /// the current state, or wait for automatic recovery. + /// + /// - Parameter toolName: Name of the tool whose circuit is open. + case circuitBreakerOpen(toolName: String) } diff --git a/Sources/AGUITools/Registry/ToolRegistry.swift b/Sources/AGUITools/Registry/ToolRegistry.swift index f64be8e..08a633c 100644 --- a/Sources/AGUITools/Registry/ToolRegistry.swift +++ b/Sources/AGUITools/Registry/ToolRegistry.swift @@ -83,6 +83,23 @@ public protocol ToolRegistry: Sendable { /// /// - Returns: Map of tool name to executor func getAllExecutors() async -> [String: any ToolExecutor] + + /// Returns the current circuit breaker state for a registered tool. + /// + /// Use this to monitor whether a tool's circuit is open, closed, or in + /// the half-open recovery probe state. Returns `nil` when the tool name + /// is not registered or the registry has no circuit breaker support. + /// + /// - Parameter toolName: The name of the tool to query. + /// - Returns: The circuit breaker state, or `nil` if unavailable. + func circuitBreakerState(for toolName: String) async -> CircuitBreakerState? +} + +extension ToolRegistry { + /// Default implementation — returns `nil` (no circuit breaker). + public func circuitBreakerState(for toolName: String) async -> CircuitBreakerState? { + nil + } } // MARK: - ToolRegistryError @@ -116,11 +133,19 @@ public enum ToolRegistryError: Error, Sendable { /// /// - SeeAlso: ``ToolRegistry``, ``ToolExecutor`` public actor DefaultToolRegistry: ToolRegistry { + private let errorHandlerConfig: ToolErrorConfig private var executors: [String: any ToolExecutor] = [:] private var stats: [String: MutableToolExecutionStats] = [:] + private var errorHandlers: [String: ToolErrorHandler] = [:] /// Creates a new empty tool registry. - public init() {} + /// + /// - Parameter errorHandlerConfig: Configuration applied to every tool's + /// circuit breaker and retry logic. Defaults to sensible production values + /// (5 failures to open, 60 s recovery, exponential-jitter retry, 3 retries). + public init(errorHandlerConfig: ToolErrorConfig = ToolErrorConfig()) { + self.errorHandlerConfig = errorHandlerConfig + } public func register(executor: any ToolExecutor) async throws { let toolName = executor.tool.name @@ -135,11 +160,13 @@ public actor DefaultToolRegistry: ToolRegistry { executors[toolName] = executor stats[toolName] = MutableToolExecutionStats() + errorHandlers[toolName] = ToolErrorHandler(config: errorHandlerConfig) } public func unregister(toolName: String) async -> Bool { let wasPresent = executors.removeValue(forKey: toolName) != nil stats.removeValue(forKey: toolName) + errorHandlers.removeValue(forKey: toolName) return wasPresent } @@ -162,29 +189,51 @@ public actor DefaultToolRegistry: ToolRegistry { throw ToolRegistryError.toolNotFound(toolName) } - let startTime = ContinuousClock.now - let result: ToolExecutionResult + guard let handler = errorHandlers[toolName] else { + throw ToolRegistryError.toolNotFound(toolName) + } - do { - // Execute with timeout if specified - if let maxTime = executor.maximumExecutionTime() { - result = try await withTimeout(maxTime, toolName: toolName) { - try await executor.execute(context: context) - } - } else { - result = try await executor.execute(context: context) + var attempt = 0 + while true { + // Fast-fail before calling the executor when the circuit is open. + guard await handler.shouldAllowExecution() else { + throw ToolExecutionError.circuitBreakerOpen(toolName: toolName) } - // Update success statistics - let duration = startTime.duration(to: .now) - stats[toolName]?.recordSuccess(duration: duration) + let startTime = ContinuousClock.now + do { + let result: ToolExecutionResult + if let maxTime = executor.maximumExecutionTime() { + result = try await withTimeout(maxTime, toolName: toolName) { + try await executor.execute(context: context) + } + } else { + result = try await executor.execute(context: context) + } - return result - } catch { - // Update failure statistics - let duration = startTime.duration(to: .now) - stats[toolName]?.recordFailure(duration: duration) - throw error + let duration = startTime.duration(to: .now) + stats[toolName]?.recordSuccess(duration: duration) + await handler.recordSuccess() + return result + } catch { + let duration = startTime.duration(to: .now) + stats[toolName]?.recordFailure(duration: duration) + + let decision = await handler.handleError( + error: error, + context: context, + attempt: attempt + ) + switch decision { + case .retry(let delayNanoseconds): + attempt += 1 + try await Task.sleep(nanoseconds: delayNanoseconds) + case .fail: + throw error + case .circuitOpen: + throw ToolExecutionError.circuitBreakerOpen(toolName: toolName) + } + } } } @@ -205,6 +254,11 @@ public actor DefaultToolRegistry: ToolRegistry { public func getAllExecutors() async -> [String: any ToolExecutor] { executors } + + public func circuitBreakerState(for toolName: String) async -> CircuitBreakerState? { + guard let handler = errorHandlers[toolName] else { return nil } + return await handler.circuitBreakerState() + } } // MARK: - MutableToolExecutionStats diff --git a/Tests/AGUIToolsTests/Core/ToolErrorHandlerTests.swift b/Tests/AGUIToolsTests/Core/ToolErrorHandlerTests.swift new file mode 100644 index 0000000..697ee8c --- /dev/null +++ b/Tests/AGUIToolsTests/Core/ToolErrorHandlerTests.swift @@ -0,0 +1,326 @@ +// Copyright (c) 2025 Perfect Aduh. MIT License. See LICENSE for details. + +import XCTest +@testable import AGUITools +@testable import AGUICore + +// MARK: - Helpers + +private func makeContext(toolName: String = "calculator") -> ToolExecutionContext { + ToolExecutionContext( + toolCall: ToolCall( + id: "call_1", + function: FunctionCall(name: toolName, arguments: "{}") + ) + ) +} + +private struct TransientError: Error {} + +// MARK: - CircuitBreakerTests + +final class CircuitBreakerTests: XCTestCase { + + // MARK: Closed state + + func test_allowsRequests_whenClosed() async { + let cb = CircuitBreaker() + let allowed = await cb.allowRequest() + XCTAssertTrue(allowed) + } + + func test_staysClosed_belowFailureThreshold() async { + let cb = CircuitBreaker(config: CircuitBreakerConfig(failureThreshold: 3)) + await cb.recordFailure() + await cb.recordFailure() + let state = await cb.currentState() + let allowed = await cb.allowRequest() + XCTAssertEqual(state, .closed) + XCTAssertTrue(allowed) + } + + func test_opensCircuit_whenFailureThresholdReached() async { + let cb = CircuitBreaker(config: CircuitBreakerConfig(failureThreshold: 2)) + await cb.recordFailure() + await cb.recordFailure() + let state = await cb.currentState() + let allowed = await cb.allowRequest() + XCTAssertEqual(state, .open) + XCTAssertFalse(allowed) + } + + func test_resetsFailureCount_onSuccessWhileClosed() async { + let cb = CircuitBreaker(config: CircuitBreakerConfig(failureThreshold: 3)) + await cb.recordFailure() + await cb.recordFailure() + await cb.recordSuccess() + // Needs 3 more failures to open + await cb.recordFailure() + await cb.recordFailure() + let state = await cb.currentState() + XCTAssertEqual(state, .closed) + } + + // MARK: Open state + + func test_blocksAllRequests_whenOpen() async { + let cb = CircuitBreaker(config: CircuitBreakerConfig(failureThreshold: 1)) + await cb.recordFailure() + let first = await cb.allowRequest() + let second = await cb.allowRequest() + XCTAssertFalse(first) + XCTAssertFalse(second) + } + + func test_transitionsToHalfOpen_afterRecoveryTimeout() async throws { + let cb = CircuitBreaker(config: CircuitBreakerConfig( + failureThreshold: 1, + recoveryTimeoutSeconds: 0.05 + )) + await cb.recordFailure() + let blockedBefore = await cb.allowRequest() + XCTAssertFalse(blockedBefore) + + try await Task.sleep(for: .milliseconds(60)) + + let allowedAfter = await cb.allowRequest() + let state = await cb.currentState() + XCTAssertTrue(allowedAfter) + XCTAssertEqual(state, .halfOpen) + } + + // MARK: Half-open state + + func test_closesCircuit_afterSuccessThresholdInHalfOpen() async throws { + let cb = CircuitBreaker(config: CircuitBreakerConfig( + failureThreshold: 1, + recoveryTimeoutSeconds: 0.05, + successThreshold: 2 + )) + await cb.recordFailure() + try await Task.sleep(for: .milliseconds(60)) + _ = await cb.allowRequest() // transitions to halfOpen + + await cb.recordSuccess() + let midState = await cb.currentState() + await cb.recordSuccess() + let finalState = await cb.currentState() + + XCTAssertEqual(midState, .halfOpen) + XCTAssertEqual(finalState, .closed) + } + + func test_reopensCircuit_onFailureDuringHalfOpen() async throws { + let cb = CircuitBreaker(config: CircuitBreakerConfig( + failureThreshold: 1, + recoveryTimeoutSeconds: 0.05 + )) + await cb.recordFailure() + try await Task.sleep(for: .milliseconds(60)) + _ = await cb.allowRequest() // transitions to halfOpen + let halfOpenState = await cb.currentState() + XCTAssertEqual(halfOpenState, .halfOpen) + + await cb.recordFailure() + let finalState = await cb.currentState() + XCTAssertEqual(finalState, .open) + } + + // MARK: Reset + + func test_reset_closesForcefully() async { + let cb = CircuitBreaker(config: CircuitBreakerConfig(failureThreshold: 1)) + await cb.recordFailure() + let openState = await cb.currentState() + XCTAssertEqual(openState, .open) + + await cb.reset() + let closedState = await cb.currentState() + let allowed = await cb.allowRequest() + XCTAssertEqual(closedState, .closed) + XCTAssertTrue(allowed) + } +} + +// MARK: - ToolErrorHandlerTests + +final class ToolErrorHandlerTests: XCTestCase { + + private let context = makeContext() + + // MARK: shouldAllowExecution + + func test_shouldAllowExecution_trueWhenClosed() async { + let handler = ToolErrorHandler() + let allowed = await handler.shouldAllowExecution() + XCTAssertTrue(allowed) + } + + func test_shouldAllowExecution_falseWhenCircuitOpen() async { + let handler = ToolErrorHandler(config: ToolErrorConfig( + maxRetryAttempts: 0, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 1) + )) + _ = await handler.handleError(error: TransientError(), context: context, attempt: 0) + let allowed = await handler.shouldAllowExecution() + XCTAssertFalse(allowed) + } + + func test_shouldAllowExecution_trueAfterRecovery() async throws { + let handler = ToolErrorHandler(config: ToolErrorConfig( + maxRetryAttempts: 0, + circuitBreaker: CircuitBreakerConfig( + failureThreshold: 1, + recoveryTimeoutSeconds: 0.05 + ) + )) + _ = await handler.handleError(error: TransientError(), context: context, attempt: 0) + let blockedBefore = await handler.shouldAllowExecution() + XCTAssertFalse(blockedBefore) + + try await Task.sleep(for: .milliseconds(60)) + + let allowedAfter = await handler.shouldAllowExecution() + let state = await handler.circuitBreakerState() + XCTAssertTrue(allowedAfter) + XCTAssertEqual(state, .halfOpen) + } + + // MARK: handleError — retry decisions + + func test_handleError_returnsRetry_forTransientError() async { + let handler = ToolErrorHandler(config: ToolErrorConfig(maxRetryAttempts: 3)) + let decision = await handler.handleError( + error: TransientError(), + context: context, + attempt: 0 + ) + guard case .retry = decision else { + XCTFail("Expected .retry, got \(decision)") + return + } + } + + func test_handleError_returnsFail_whenMaxAttemptsExceeded() async { + let handler = ToolErrorHandler(config: ToolErrorConfig(maxRetryAttempts: 2)) + let decision = await handler.handleError( + error: TransientError(), + context: context, + attempt: 2 + ) + guard case .fail = decision else { + XCTFail("Expected .fail, got \(decision)") + return + } + } + + func test_handleError_returnsFail_forNonRetryableValidationError() async { + let handler = ToolErrorHandler(config: ToolErrorConfig(retryOnValidation: false)) + let decision = await handler.handleError( + error: ToolExecutionError.validationFailed(message: "bad args"), + context: context, + attempt: 0 + ) + guard case .fail = decision else { + XCTFail("Expected .fail, got \(decision)") + return + } + } + + func test_handleError_returnsRetry_forValidationErrorWhenConfigured() async { + let handler = ToolErrorHandler(config: ToolErrorConfig( + maxRetryAttempts: 3, + retryOnValidation: true + )) + let decision = await handler.handleError( + error: ToolExecutionError.validationFailed(message: "bad args"), + context: context, + attempt: 0 + ) + guard case .retry = decision else { + XCTFail("Expected .retry, got \(decision)") + return + } + } + + // MARK: handleError — circuit breaker integration + + func test_handleError_opensCircuit_afterFailureThreshold() async { + let handler = ToolErrorHandler(config: ToolErrorConfig( + maxRetryAttempts: 10, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 3) + )) + for attempt in 0 ..< 3 { + _ = await handler.handleError(error: TransientError(), context: context, attempt: attempt) + } + let state = await handler.circuitBreakerState() + XCTAssertEqual(state, .open) + } + + func test_recordSuccess_closesCircuit_afterRecovery() async throws { + let handler = ToolErrorHandler(config: ToolErrorConfig( + maxRetryAttempts: 0, + circuitBreaker: CircuitBreakerConfig( + failureThreshold: 1, + recoveryTimeoutSeconds: 0.05, + successThreshold: 1 + ) + )) + _ = await handler.handleError(error: TransientError(), context: context, attempt: 0) + let openState = await handler.circuitBreakerState() + XCTAssertEqual(openState, .open) + + try await Task.sleep(for: .milliseconds(60)) + _ = await handler.shouldAllowExecution() // transitions to halfOpen + + await handler.recordSuccess() + let finalState = await handler.circuitBreakerState() + XCTAssertEqual(finalState, .closed) + } + + // MARK: Retry delay strategies + + func test_fixedStrategy_returnsConstantDelay() async { + let handler = ToolErrorHandler(config: ToolErrorConfig( + maxRetryAttempts: 5, + baseRetryDelayMs: 100, + retryStrategy: .fixed + )) + var delays: [UInt64] = [] + for attempt in 0 ..< 3 { + let decision = await handler.handleError( + error: TransientError(), context: context, attempt: attempt + ) + if case .retry(let ns) = decision { + delays.append(ns) + } + } + XCTAssertEqual(delays.count, 3) + let expected: UInt64 = 100 * 1_000_000 + for delay in delays { + XCTAssertEqual(delay, expected) + } + } + + func test_exponentialStrategy_doublesDelay() async { + let handler = ToolErrorHandler(config: ToolErrorConfig( + maxRetryAttempts: 5, + baseRetryDelayMs: 100, + maxRetryDelayMs: 10_000, + retryStrategy: .exponential + )) + var delays: [UInt64] = [] + for attempt in 0 ..< 3 { + let decision = await handler.handleError( + error: TransientError(), context: context, attempt: attempt + ) + if case .retry(let ns) = decision { + delays.append(ns) + } + } + // attempt 0 → 100ms, attempt 1 → 200ms, attempt 2 → 400ms + XCTAssertEqual(delays[0], 100 * 1_000_000) + XCTAssertEqual(delays[1], 200 * 1_000_000) + XCTAssertEqual(delays[2], 400 * 1_000_000) + } +} diff --git a/Tests/AGUIToolsTests/Registry/CircuitBreakerIntegrationTests.swift b/Tests/AGUIToolsTests/Registry/CircuitBreakerIntegrationTests.swift new file mode 100644 index 0000000..3b4b7b6 --- /dev/null +++ b/Tests/AGUIToolsTests/Registry/CircuitBreakerIntegrationTests.swift @@ -0,0 +1,257 @@ +// Copyright (c) 2025 Perfect Aduh. MIT License. See LICENSE for details. + +import XCTest +@testable import AGUITools +@testable import AGUICore + +/// Integration tests that verify the circuit breaker works end-to-end through +/// ``DefaultToolRegistry/execute(context:)``. +final class CircuitBreakerIntegrationTests: XCTestCase { + + // MARK: - Helpers + + private let toolName = "weather" + private struct NetworkError: Error {} + + private func makeTool(name: String? = nil) -> Tool { + Tool(name: name ?? toolName, description: "Gets weather", parameters: Data("{}".utf8)) + } + + private func makeContext(name: String? = nil) -> ToolExecutionContext { + ToolExecutionContext( + toolCall: ToolCall( + id: "call_1", + function: FunctionCall(name: name ?? toolName, arguments: "{}") + ) + ) + } + + // Executor that fails a configurable number of times then succeeds. + actor CountingExecutor: ToolExecutor { + let tool: Tool + private(set) var callCount = 0 + private let failUntil: Int + + init(tool: Tool, failUntil: Int = Int.max) { + self.tool = tool + self.failUntil = failUntil + } + + func execute(context: ToolExecutionContext) async throws -> ToolExecutionResult { + callCount += 1 + if callCount <= failUntil { + throw NetworkError() + } + return ToolExecutionResult.success(message: "ok") + } + + nonisolated func validate(toolCall: ToolCall) -> ToolValidationResult { .valid } + nonisolated func maximumExecutionTime() -> Duration? { nil } + } + + // MARK: - Circuit Breaker State Exposure + + func test_circuitBreakerState_isClosedForNewTool() async throws { + let registry = DefaultToolRegistry() + try await registry.register(executor: TestToolExecutor(tool: makeTool())) + + let state = await registry.circuitBreakerState(for: toolName) + XCTAssertEqual(state, .closed) + } + + func test_circuitBreakerState_isNilForUnregisteredTool() async { + let registry = DefaultToolRegistry() + let state = await registry.circuitBreakerState(for: "unknown") + XCTAssertNil(state) + } + + func test_circuitBreakerState_isRemovedOnUnregister() async throws { + let registry = DefaultToolRegistry() + try await registry.register(executor: TestToolExecutor(tool: makeTool())) + _ = await registry.unregister(toolName: toolName) + + let state = await registry.circuitBreakerState(for: toolName) + XCTAssertNil(state) + } + + // MARK: - Circuit Breaker Opens After Failures + + func test_circuitOpens_afterFailureThreshold() async throws { + let config = ToolErrorConfig( + maxRetryAttempts: 0, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 3) + ) + let registry = DefaultToolRegistry(errorHandlerConfig: config) + let executor = CountingExecutor(tool: makeTool()) + try await registry.register(executor: executor) + + for _ in 0 ..< 3 { + do { _ = try await registry.execute(context: makeContext()) } catch {} + } + + let state = await registry.circuitBreakerState(for: toolName) + XCTAssertEqual(state, .open) + } + + // MARK: - Fast-Fail When Circuit Is Open + + func test_execute_throwsCircuitBreakerOpen_whenCircuitIsOpen() async throws { + let config = ToolErrorConfig( + maxRetryAttempts: 0, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 1) + ) + let registry = DefaultToolRegistry(errorHandlerConfig: config) + let executor = CountingExecutor(tool: makeTool()) + try await registry.register(executor: executor) + + // Trip the circuit + do { _ = try await registry.execute(context: makeContext()) } catch {} + let stateAfterTrip = await registry.circuitBreakerState(for: toolName) + XCTAssertEqual(stateAfterTrip, .open) + + let callCountBeforeFastFail = await executor.callCount + + // Next call should fast-fail before reaching the executor + do { + _ = try await registry.execute(context: makeContext()) + XCTFail("Expected circuitBreakerOpen to be thrown") + } catch let error as ToolExecutionError { + guard case .circuitBreakerOpen(let name) = error else { + XCTFail("Expected .circuitBreakerOpen, got \(error)") + return + } + XCTAssertEqual(name, toolName) + } + + let callCountAfterFastFail = await executor.callCount + XCTAssertEqual(callCountAfterFastFail, callCountBeforeFastFail, + "Executor must not be called during circuit-open fast-fail") + } + + // MARK: - Retry Logic + + func test_execute_retries_onTransientFailure_thenSucceeds() async throws { + let config = ToolErrorConfig( + maxRetryAttempts: 2, + baseRetryDelayMs: 1, + retryStrategy: .fixed, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 10) + ) + let registry = DefaultToolRegistry(errorHandlerConfig: config) + // Fails first 2 times, succeeds on 3rd + let executor = CountingExecutor(tool: makeTool(), failUntil: 2) + try await registry.register(executor: executor) + + let result = try await registry.execute(context: makeContext()) + let finalCallCount = await executor.callCount + + XCTAssertTrue(result.success) + XCTAssertEqual(finalCallCount, 3) + } + + func test_execute_throwsAfterExhaustingRetries() async throws { + let config = ToolErrorConfig( + maxRetryAttempts: 2, + baseRetryDelayMs: 1, + retryStrategy: .fixed, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 10) + ) + let registry = DefaultToolRegistry(errorHandlerConfig: config) + let executor = CountingExecutor(tool: makeTool()) // always fails + try await registry.register(executor: executor) + + do { + _ = try await registry.execute(context: makeContext()) + XCTFail("Expected an error to be thrown") + } catch { + // Expected — verify the executor was called initial + 2 retries = 3 times + let callCount = await executor.callCount + XCTAssertEqual(callCount, 3) + } + } + + // MARK: - Recovery After Timeout + + func test_execute_recovers_afterCircuitOpenTimeout() async throws { + let config = ToolErrorConfig( + maxRetryAttempts: 0, + circuitBreaker: CircuitBreakerConfig( + failureThreshold: 1, + recoveryTimeoutSeconds: 0.05, + successThreshold: 1 + ) + ) + let registry = DefaultToolRegistry(errorHandlerConfig: config) + // Fails once then succeeds + let executor = CountingExecutor(tool: makeTool(), failUntil: 1) + try await registry.register(executor: executor) + + // Open the circuit + do { _ = try await registry.execute(context: makeContext()) } catch {} + let openState = await registry.circuitBreakerState(for: toolName) + XCTAssertEqual(openState, .open) + + // Confirm fast-fail while open + do { + _ = try await registry.execute(context: makeContext()) + XCTFail("Expected circuitBreakerOpen") + } catch is ToolExecutionError {} + + // Wait for recovery window + try await Task.sleep(for: .milliseconds(60)) + + // Probe should succeed and close the circuit + let result = try await registry.execute(context: makeContext()) + XCTAssertTrue(result.success) + let recoveredState = await registry.circuitBreakerState(for: toolName) + XCTAssertEqual(recoveredState, .closed) + } + + // MARK: - Per-Tool Isolation + + func test_circuitBreaker_isIsolatedPerTool() async throws { + let config = ToolErrorConfig( + maxRetryAttempts: 0, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 1) + ) + let registry = DefaultToolRegistry(errorHandlerConfig: config) + + let alwaysFails = CountingExecutor(tool: makeTool(name: "failing")) + let alwaysWorks = CountingExecutor(tool: makeTool(name: "working"), failUntil: 0) + try await registry.register(executor: alwaysFails) + try await registry.register(executor: alwaysWorks) + + // Open the failing tool's circuit + do { _ = try await registry.execute(context: makeContext(name: "failing")) } catch {} + let failingState = await registry.circuitBreakerState(for: "failing") + XCTAssertEqual(failingState, .open) + + // Working tool should still execute normally + let result = try await registry.execute(context: makeContext(name: "working")) + XCTAssertTrue(result.success) + let workingState = await registry.circuitBreakerState(for: "working") + XCTAssertEqual(workingState, .closed) + } + + // MARK: - Stats Accuracy Under Retries + + func test_stats_countEachAttemptIndividually() async throws { + let config = ToolErrorConfig( + maxRetryAttempts: 2, + baseRetryDelayMs: 1, + retryStrategy: .fixed, + circuitBreaker: CircuitBreakerConfig(failureThreshold: 10) + ) + let registry = DefaultToolRegistry(errorHandlerConfig: config) + let executor = CountingExecutor(tool: makeTool()) // always fails + try await registry.register(executor: executor) + + do { _ = try await registry.execute(context: makeContext()) } catch {} + + let stats = await registry.stats(for: toolName) + XCTAssertNotNil(stats) + // Initial attempt + 2 retries = 3 failures recorded + XCTAssertEqual(stats?.failureCount, 3) + XCTAssertEqual(stats?.successCount, 0) + } +}