diff --git a/CHANGELOG.md b/CHANGELOG.md index e515800..4aeaf8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to SwiftAgentKit will be documented in this file. ## Unreleased ### Added +- **Autonomous mode.** `AgentConfig(autonomousMode:)` (default `false`) and a runtime `Agent.setAutonomousMode(_:)` / `ToolDispatcher.setAutonomousMode(_:)` let an app skip the `requiresConfirmation` gate so the agent runs confirmation-required tools without prompting `AgentCallbacks.onToolConfirmation`. Off by default (confirmation-gated / fail-closed unchanged); flip it on for full-autonomy runs. Regression coverage added (autonomous mode bypasses the gate with no handler present). - **Context management (ContextSift), opt-in.** A new `ContextManager` (in `Sources/SwiftAgentKit/Context/`) keeps the main user/assistant conversation intact while externalizing *completed* tool exchanges: each finished tool result becomes a compact one-line receipt in a tool ledger (folded into the system prompt), and its full output is offloaded to an `ArtifactStore` (default `InMemoryArtifactStore`) so it can be retrieved losslessly on demand rather than resent every turn. The single in-flight ("active") tool exchange is preserved in full (bounded by `maxActiveResultChars`), so the model always has what it needs to act next. Two retrieval tools — `artifact_read` (paged) and `artifact_search` (line-level substring) — are auto-registered when a `ContextManager` is set, letting the model pull back any offloaded output by its `artifact-…` id. Enable per-agent via `AgentConfig(contextManager:)`; leaving it `nil` preserves the previous behavior exactly (`makeLLMRequest` is unchanged when unset). Ports the Python ContextSift approach (compact receipts + artifact store + on-demand retrieval) into the framework. Regression coverage added (completed exchanges → ledger + artifact and dropped from context; active exchange kept; large results spilled and round-tripped via `artifact_read`; artifact store read/search). - Guard against overlapping `run(_:)` calls on the same `Agent` instance with `AgentError.runInProgress`. - Regression coverage for same-instance concurrent run rejection. diff --git a/Sources/SwiftAgentKit/Core/Agent.swift b/Sources/SwiftAgentKit/Core/Agent.swift index 72885c3..4cf21f2 100644 --- a/Sources/SwiftAgentKit/Core/Agent.swift +++ b/Sources/SwiftAgentKit/Core/Agent.swift @@ -68,6 +68,12 @@ public struct AgentConfig: Sendable { /// agent uses its normal trim-based context handling. public var contextManager: ContextManager? + /// When `true`, tools marked `requiresConfirmation` run WITHOUT prompting via + /// `AgentCallbacks.onToolConfirmation` — the agent has full autonomy. Default + /// `false` (confirmation-gated). Can also be flipped at runtime with + /// `agent.setAutonomousMode(_:)`. + public var autonomousMode: Bool + public init( provider: any LLMProvider, model: String? = nil, @@ -82,7 +88,8 @@ public struct AgentConfig: Sendable { enableRepairRetry: Bool = true, enablePlanContinuation: Bool = true, tools: [any AgentTool] = [], - contextManager: ContextManager? = nil + contextManager: ContextManager? = nil, + autonomousMode: Bool = false ) { self.provider = provider self.model = model @@ -98,6 +105,7 @@ public struct AgentConfig: Sendable { self.enablePlanContinuation = enablePlanContinuation self.tools = tools self.contextManager = contextManager + self.autonomousMode = autonomousMode } } @@ -263,10 +271,21 @@ public final class Agent: @unchecked Sendable { register(tool) } } + + // Apply autonomous mode (skips the confirmation gate) if configured. + if config.autonomousMode { + setAutonomousMode(true) + } } // MARK: - Tools + /// Enable/disable autonomous mode at runtime. When `true`, tools marked + /// `requiresConfirmation` run without prompting `onToolConfirmation`. + public func setAutonomousMode(_ enabled: Bool) { + trackRegistrationTask(Task { await dispatcher.setAutonomousMode(enabled) }) + } + /// Register a tool. public func register(_ tool: any AgentTool) { trackRegistrationTask(Task { await tools.register(tool) }) diff --git a/Sources/SwiftAgentKit/Tools/ToolDispatcher.swift b/Sources/SwiftAgentKit/Tools/ToolDispatcher.swift index 4e0eec1..5b4f4c2 100644 --- a/Sources/SwiftAgentKit/Tools/ToolDispatcher.swift +++ b/Sources/SwiftAgentKit/Tools/ToolDispatcher.swift @@ -45,6 +45,11 @@ public actor ToolDispatcher { contextFields = context } + /// Enable/disable autonomous mode (skips the `requiresConfirmation` gate). + public func setAutonomousMode(_ enabled: Bool) { + autonomousMode = enabled + } + // MARK: - Dispatch /// Dispatch a batch of tool calls — optionally in parallel. diff --git a/Tests/SwiftAgentKitTests/SwiftAgentKitTests.swift b/Tests/SwiftAgentKitTests/SwiftAgentKitTests.swift index 8c2c0bd..d00271f 100644 --- a/Tests/SwiftAgentKitTests/SwiftAgentKitTests.swift +++ b/Tests/SwiftAgentKitTests/SwiftAgentKitTests.swift @@ -292,6 +292,21 @@ struct DangerousTool: AgentTool { #expect(results[0].result.contains("not approved")) } +@Test func testAutonomousModeBypassesConfirmationGate() async { + let registry = ToolRegistry() + await registry.register(DangerousTool()) + let dispatcher = ToolDispatcher(registry: registry) + await dispatcher.setAutonomousMode(true) + let state = AgentState() + + // No onToolConfirmation handler, but autonomous mode is on → runs anyway. + let call = AgentToolCall(name: "delete_everything") + let results = await dispatcher.dispatch(calls: [call], state: state, observer: nil) + #expect(results.count == 1) + #expect(results[0].isError == false) + #expect(results[0].result == "done") +} + // MARK: - Conversation/Memory Tests @Test func testConversationAppendAndRead() {