feat(10-02c): add InProcessConfirmationCache for sync confirmation lookup
- Lock-protected Set<String> mirror of registered tools' requiresConfirmation flags. - InProcessToolRegistry.register(_:) now writes through to the cache so the upcoming InProcessAwareToolDispatcher can answer requiresConfirmation(toolName:) synchronously from a nonisolated context (ToolDispatcher protocol contract). - OSAllocatedUnfairLock: low-overhead, available since macOS 13.
This commit is contained in:
1 file changed
+56
-1
@@ -1,6 +1,50 @@
|
||||
import Foundation
|
||||
import os.lock
|
||||
import AgentCore
|
||||
|
||||
/// Sync-accessible cache of in-process tool names that require confirmation.
|
||||
///
|
||||
/// Plan 10-02c (B-01b): the new `InProcessAwareToolDispatcher` composite
|
||||
/// must answer `nonisolated func requiresConfirmation(toolName:)` synchronously
|
||||
/// to satisfy the `ToolDispatcher` protocol contract that the surrounding
|
||||
/// `ConfirmingToolDispatcher` already calls into. Reading from the actor-bound
|
||||
/// `InProcessToolRegistry.tools` would require an `await`, which the protocol
|
||||
/// doesn't allow. This lock-protected `Set<String>` mirrors the registry's
|
||||
/// confirmation flags and is updated by `register(_:)`.
|
||||
///
|
||||
/// Class (not struct) so the registry and the composite share a single
|
||||
/// reference; class is `@unchecked Sendable` because all mutation is gated
|
||||
/// by the unfair lock.
|
||||
public final class InProcessConfirmationCache: @unchecked Sendable {
|
||||
private let state = OSAllocatedUnfairLock(initialState: Set<String>())
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Insert `name` if `requiresConfirmation` is true; otherwise remove it.
|
||||
/// Idempotent — registering the same tool twice (re-keying) keeps the
|
||||
/// cache consistent with the registry's last-write-wins semantics.
|
||||
public func insert(_ name: String, requiresConfirmation: Bool) {
|
||||
state.withLock { set in
|
||||
if requiresConfirmation {
|
||||
set.insert(name)
|
||||
} else {
|
||||
set.remove(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync read — used by `InProcessAwareToolDispatcher.requiresConfirmation(toolName:)`
|
||||
/// from a `nonisolated` context.
|
||||
public func contains(_ name: String) -> Bool {
|
||||
state.withLock { $0.contains(name) }
|
||||
}
|
||||
|
||||
/// Snapshot for tests.
|
||||
public func names() -> Set<String> {
|
||||
state.withLock { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds [String: any InProcessTool] and dispatches CallTool requests by name.
|
||||
/// AppDelegate (Plan 07-06) builds one and injects it alongside the existing
|
||||
/// MCPRuntimeWiring helper-spawn registration.
|
||||
@@ -8,10 +52,21 @@ public actor InProcessToolRegistry {
|
||||
|
||||
private var tools: [String: any InProcessTool] = [:]
|
||||
|
||||
public init() {}
|
||||
/// Plan 10-02c (B-01b): sync-accessible mirror of which registered tools
|
||||
/// require confirmation. Shared with `InProcessAwareToolDispatcher` so
|
||||
/// its `nonisolated func requiresConfirmation(toolName:)` can answer
|
||||
/// synchronously without an actor hop. Updated by `register(_:)`.
|
||||
public nonisolated let confirmationCache: InProcessConfirmationCache
|
||||
|
||||
public init() {
|
||||
self.confirmationCache = InProcessConfirmationCache()
|
||||
}
|
||||
|
||||
public func register(_ tool: any InProcessTool) {
|
||||
tools[tool.name] = tool
|
||||
// Mirror the confirmation flag into the sync cache so the composite
|
||||
// dispatcher can read it from a nonisolated context.
|
||||
confirmationCache.insert(tool.name, requiresConfirmation: tool.requiresConfirmation)
|
||||
}
|
||||
|
||||
public func registered() -> [any InProcessTool] {
|
||||
|
||||
Reference in new issue
Block a user