// Sources/Core/BackgroundManager.swift
public actor BackgroundManager { private let executor: ShellExecutor private var jobs: [String: BackgroundJob] = [:] private var notifications: [BackgroundNotification] = [] private var runningTasks: [String: Task<Void, Never>] = [:] public init(executor: ShellExecutor) { self.executor = executor }
}
// Sources/Core/BackgroundManager.swift
public actor BackgroundManager { private let executor: ShellExecutor private var jobs: [String: BackgroundJob] = [:] private var notifications: [BackgroundNotification] = [] private var runningTasks: [String: Task<Void, Never>] = [:] public init(executor: ShellExecutor) { self.executor = executor }
}
// Sources/Core/BackgroundManager.swift
public actor BackgroundManager { private let executor: ShellExecutor private var jobs: [String: BackgroundJob] = [:] private var notifications: [BackgroundNotification] = [] private var runningTasks: [String: Task<Void, Never>] = [:] public init(executor: ShellExecutor) { self.executor = executor }
}
public struct BackgroundJob: Sendable, Equatable { public let id: String public let command: String public let commandPreview: String public var status: BackgroundJobStatus public var result: String?
}
public struct BackgroundJob: Sendable, Equatable { public let id: String public let command: String public let commandPreview: String public var status: BackgroundJobStatus public var result: String?
}
public struct BackgroundJob: Sendable, Equatable { public let id: String public let command: String public let commandPreview: String public var status: BackgroundJobStatus public var result: String?
}
public struct BackgroundNotification: Sendable, Equatable { public let jobId: String public let status: BackgroundJobStatus public let command: String public let result: String
}
public struct BackgroundNotification: Sendable, Equatable { public let jobId: String public let status: BackgroundJobStatus public let command: String public let result: String
}
public struct BackgroundNotification: Sendable, Equatable { public let jobId: String public let status: BackgroundJobStatus public let command: String public let result: String
}
public func run( command: String, timeout: TimeInterval = Limits.backgroundTimeout
) -> String { let jobId = String(UUID().uuidString.prefix(8)).lowercased() let commandPreview = String(command.prefix(Limits.backgroundCommandPreview)) jobs[jobId] = BackgroundJob( id: jobId, command: command, commandPreview: commandPreview, status: .running ) let task = Task { let status: BackgroundJobStatus let output: String do { let result = try await self.executor.execute(command, timeout: timeout) if result.exitCode != 0 { status = .error } else { status = .completed } output = result.formatted } catch ShellExecutorError.timeout { status = .timeout output = "Error: Timeout (\(Int(timeout))s)" } catch { status = .error output = "Error: \(error)" } self.complete(jobId: jobId, status: status, output: output) } runningTasks[jobId] = task return "Background job \(jobId) started: \(commandPreview)"
}
public func run( command: String, timeout: TimeInterval = Limits.backgroundTimeout
) -> String { let jobId = String(UUID().uuidString.prefix(8)).lowercased() let commandPreview = String(command.prefix(Limits.backgroundCommandPreview)) jobs[jobId] = BackgroundJob( id: jobId, command: command, commandPreview: commandPreview, status: .running ) let task = Task { let status: BackgroundJobStatus let output: String do { let result = try await self.executor.execute(command, timeout: timeout) if result.exitCode != 0 { status = .error } else { status = .completed } output = result.formatted } catch ShellExecutorError.timeout { status = .timeout output = "Error: Timeout (\(Int(timeout))s)" } catch { status = .error output = "Error: \(error)" } self.complete(jobId: jobId, status: status, output: output) } runningTasks[jobId] = task return "Background job \(jobId) started: \(commandPreview)"
}
public func run( command: String, timeout: TimeInterval = Limits.backgroundTimeout
) -> String { let jobId = String(UUID().uuidString.prefix(8)).lowercased() let commandPreview = String(command.prefix(Limits.backgroundCommandPreview)) jobs[jobId] = BackgroundJob( id: jobId, command: command, commandPreview: commandPreview, status: .running ) let task = Task { let status: BackgroundJobStatus let output: String do { let result = try await self.executor.execute(command, timeout: timeout) if result.exitCode != 0 { status = .error } else { status = .completed } output = result.formatted } catch ShellExecutorError.timeout { status = .timeout output = "Error: Timeout (\(Int(timeout))s)" } catch { status = .error output = "Error: \(error)" } self.complete(jobId: jobId, status: status, output: output) } runningTasks[jobId] = task return "Background job \(jobId) started: \(commandPreview)"
}
private func complete( jobId: String, status: BackgroundJobStatus, output: String
) { jobs[jobId]?.status = status jobs[jobId]?.result = output notifications.append( BackgroundNotification( jobId: jobId, status: status, command: jobs[jobId]?.commandPreview ?? "", result: String(output.prefix(Limits.backgroundResultPreview)) ) ) runningTasks.removeValue(forKey: jobId)
}
private func complete( jobId: String, status: BackgroundJobStatus, output: String
) { jobs[jobId]?.status = status jobs[jobId]?.result = output notifications.append( BackgroundNotification( jobId: jobId, status: status, command: jobs[jobId]?.commandPreview ?? "", result: String(output.prefix(Limits.backgroundResultPreview)) ) ) runningTasks.removeValue(forKey: jobId)
}
private func complete( jobId: String, status: BackgroundJobStatus, output: String
) { jobs[jobId]?.status = status jobs[jobId]?.result = output notifications.append( BackgroundNotification( jobId: jobId, status: status, command: jobs[jobId]?.commandPreview ?? "", result: String(output.prefix(Limits.backgroundResultPreview)) ) ) runningTasks.removeValue(forKey: jobId)
}
public func drainNotifications() -> [BackgroundNotification] { let result = notifications notifications.removeAll() return result
}
public func drainNotifications() -> [BackgroundNotification] { let result = notifications notifications.removeAll() return result
}
public func drainNotifications() -> [BackgroundNotification] { let result = notifications notifications.removeAll() return result
}
func drainBackgroundNotifications(_ messages: [Message]) async -> [Message] { let notifications = await backgroundManager.drainNotifications() guard !notifications.isEmpty else { return messages } let text = notifications .map { "[bg:\($0.jobId)] \($0.status.rawValue): \($0.result)" } .joined(separator: "\n") var result = messages let wrappedText = "<background-results>\n\(text)\n</background-results>" if let lastMessage = result.last, lastMessage.role == .user { var updatedContent = lastMessage.content updatedContent.append(.text(wrappedText)) result[result.count - 1] = Message(role: .user, content: updatedContent) } else { result.append(.user(wrappedText)) } result.append(.assistant("Noted background results.")) return result
}
func drainBackgroundNotifications(_ messages: [Message]) async -> [Message] { let notifications = await backgroundManager.drainNotifications() guard !notifications.isEmpty else { return messages } let text = notifications .map { "[bg:\($0.jobId)] \($0.status.rawValue): \($0.result)" } .joined(separator: "\n") var result = messages let wrappedText = "<background-results>\n\(text)\n</background-results>" if let lastMessage = result.last, lastMessage.role == .user { var updatedContent = lastMessage.content updatedContent.append(.text(wrappedText)) result[result.count - 1] = Message(role: .user, content: updatedContent) } else { result.append(.user(wrappedText)) } result.append(.assistant("Noted background results.")) return result
}
func drainBackgroundNotifications(_ messages: [Message]) async -> [Message] { let notifications = await backgroundManager.drainNotifications() guard !notifications.isEmpty else { return messages } let text = notifications .map { "[bg:\($0.jobId)] \($0.status.rawValue): \($0.result)" } .joined(separator: "\n") var result = messages let wrappedText = "<background-results>\n\(text)\n</background-results>" if let lastMessage = result.last, lastMessage.role == .user { var updatedContent = lastMessage.content updatedContent.append(.text(wrappedText)) result[result.count - 1] = Message(role: .user, content: updatedContent) } else { result.append(.user(wrappedText)) } result.append(.assistant("Noted background results.")) return result
}
while true { try Task.checkCancellation() // ... messages = await applyCompaction(messages) if config.drainBackground { messages = await drainBackgroundNotifications(messages) } let request = APIRequest( model: model, maxTokens: Limits.defaultMaxTokens, system: systemPrompt, messages: messages, tools: config.tools ) let response = try await apiClient.createMessage(request: request) // ... process tools, append results, continue
}
while true { try Task.checkCancellation() // ... messages = await applyCompaction(messages) if config.drainBackground { messages = await drainBackgroundNotifications(messages) } let request = APIRequest( model: model, maxTokens: Limits.defaultMaxTokens, system: systemPrompt, messages: messages, tools: config.tools ) let response = try await apiClient.createMessage(request: request) // ... process tools, append results, continue
}
while true { try Task.checkCancellation() // ... messages = await applyCompaction(messages) if config.drainBackground { messages = await drainBackgroundNotifications(messages) } let request = APIRequest( model: model, maxTokens: Limits.defaultMaxTokens, system: systemPrompt, messages: messages, tools: config.tools ) let response = try await apiClient.createMessage(request: request) // ... process tools, append results, continue
}
static let `default` = LoopConfig( tools: Agent.toolDefinitions, maxIterations: .max, enableNag: true, drainBackground: true, label: "agent"
) static let subagent = LoopConfig( tools: Agent.toolDefinitions.filter { !subagentExcludedTools.contains($0.name) }, maxIterations: 30, enableNag: false, drainBackground: false, label: "subagent"
)
static let `default` = LoopConfig( tools: Agent.toolDefinitions, maxIterations: .max, enableNag: true, drainBackground: true, label: "agent"
) static let subagent = LoopConfig( tools: Agent.toolDefinitions.filter { !subagentExcludedTools.contains($0.name) }, maxIterations: 30, enableNag: false, drainBackground: false, label: "subagent"
)
static let `default` = LoopConfig( tools: Agent.toolDefinitions, maxIterations: .max, enableNag: true, drainBackground: true, label: "agent"
) static let subagent = LoopConfig( tools: Agent.toolDefinitions.filter { !subagentExcludedTools.contains($0.name) }, maxIterations: 30, enableNag: false, drainBackground: false, label: "subagent"
)
let startTime = DispatchTime.now()
// ... process runs, timer fires interrupt() if needed ...
process.waitUntilExit()
timer?.cancel() if let timeout { let elapsedSeconds = Double( DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds ) / 1_000_000_000 if elapsedSeconds >= timeout { throw ShellExecutorError.timeout(seconds: Int(timeout)) }
}
let startTime = DispatchTime.now()
// ... process runs, timer fires interrupt() if needed ...
process.waitUntilExit()
timer?.cancel() if let timeout { let elapsedSeconds = Double( DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds ) / 1_000_000_000 if elapsedSeconds >= timeout { throw ShellExecutorError.timeout(seconds: Int(timeout)) }
}
let startTime = DispatchTime.now()
// ... process runs, timer fires interrupt() if needed ...
process.waitUntilExit()
timer?.cancel() if let timeout { let elapsedSeconds = Double( DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds ) / 1_000_000_000 if elapsedSeconds >= timeout { throw ShellExecutorError.timeout(seconds: Int(timeout)) }
}
swift build && swift run agent
swift build && swift run agent
swift build && swift run agent - Part 0: Bootstrapping the project
- Part 1: The agent loop
- Part 2: Tool dispatch
- Part 3: Self-managed task tracking
- Part 4: Subagents
- Part 5: Skill loading
- Part 6: Context compaction
- Part 7: Task system
- Part 8: Background tasks ← you are here