Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 20 additions & 1 deletion Sources/SwiftAgentKit/Core/Agent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -98,6 +105,7 @@ public struct AgentConfig: Sendable {
self.enablePlanContinuation = enablePlanContinuation
self.tools = tools
self.contextManager = contextManager
self.autonomousMode = autonomousMode
}
}

Expand Down Expand Up @@ -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) })
Expand Down
5 changes: 5 additions & 0 deletions Sources/SwiftAgentKit/Tools/ToolDispatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions Tests/SwiftAgentKitTests/SwiftAgentKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading