22import Foundation
33
44public enum Limits {
5+ // Output/API
56 public static let maxOutputSize = 50_000
6- public static let defaultTokenThreshold = 50_000
77 public static let defaultMaxTokens = 4096
88 static let logPreviewLength = 200
9+ // Compaction
10+ public static let defaultTokenThreshold = 50_000
11+ // Background
12+ public static let backgroundTimeout : TimeInterval = 300
13+ public static let backgroundCommandPreview = 80
14+ public static let backgroundResultPreview = 500
915}
1016
1117public final class Agent {
12- public static let version = " 0.7 .0 "
18+ public static let version = " 0.8 .0 "
1319
1420 private static let todoReminderThreshold = 3
1521
@@ -23,6 +29,7 @@ public final class Agent {
2329 private let contextCompactor : ContextCompactor
2430 private let todoManager : TodoManager
2531 private let taskManager : TaskManager
32+ let backgroundManager : BackgroundManager
2633
2734 private var messages : [ Message ] = [ ]
2835
@@ -45,6 +52,7 @@ public final class Agent {
4552 )
4653 self . todoManager = TodoManager ( )
4754 self . taskManager = TaskManager ( directory: " \( workingDirectory) /.tasks " )
55+ self . backgroundManager = BackgroundManager ( executor: self . shellExecutor)
4856 self . systemPrompt =
4957 systemPrompt
5058 ?? Self . buildSystemPrompt (
@@ -71,19 +79,21 @@ public final class Agent {
7179 var messages = initialMessages
7280 var turnsWithoutTodo = 0
7381 var iteration = 0
74- var lastAssistantText = " "
75-
7682 let allowedTools = Set ( config. tools. map ( \. name) )
7783
7884 while true {
7985 try Task . checkCancellation ( )
8086
8187 iteration += 1
8288 if iteration > config. maxIterations {
83- return ( lastAssistantText + " \n ( \( config. label) reached iteration limit) " , messages)
89+ let lastText = messages. last? . content. textContent ?? " "
90+ return ( lastText + " \n ( \( config. label) reached iteration limit) " , messages)
8491 }
8592
8693 messages = await applyCompaction ( messages)
94+ if config. drainBackground {
95+ messages = await drainBackgroundNotifications ( messages)
96+ }
8797
8898 let request = APIRequest (
8999 model: model,
@@ -95,12 +105,9 @@ public final class Agent {
95105
96106 let response = try await apiClient. createMessage ( request: request)
97107 messages. append ( Message ( role: . assistant, content: response. content) )
98- lastAssistantText = response. content. textContent
99108
100- for block in response. content {
101- if case . text( let text) = block {
102- print ( " [ \( config. label) ] \( ANSIColor . cyan) \( text) \( ANSIColor . reset) " )
103- }
109+ for case . text( let text) in response. content {
110+ print ( " [ \( config. label) ] \( ANSIColor . cyan) \( text) \( ANSIColor . reset) " )
104111 }
105112
106113 guard response. stopReason == . toolUse else {
@@ -132,8 +139,6 @@ public final class Agent {
132139 }
133140 }
134141
135- // MARK: - Compaction
136-
137142 private func applyCompaction( _ messages: [ Message ] ) async -> [ Message ] {
138143 var compacted = messages
139144 contextCompactor. microCompact ( messages: & compacted)
@@ -148,6 +153,36 @@ public final class Agent {
148153 return compacted
149154 }
150155
156+ func drainBackgroundNotifications( _ messages: [ Message ] ) async -> [ Message ] {
157+ let notifications = await backgroundManager. drainNotifications ( )
158+ guard !notifications. isEmpty else {
159+ return messages
160+ }
161+
162+ let text =
163+ notifications
164+ . map { " [bg: \( $0. jobId) ] \( $0. status. rawValue) : \( $0. result) " }
165+ . joined ( separator: " \n " )
166+
167+ var result = messages
168+ let wrappedText = " <background-results> \n \( text) \n </background-results> "
169+
170+ // Avoid consecutive user messages (violates API alternation requirement).
171+ // If last message is user, append to its content; otherwise add new user message.
172+ if let lastMessage = result. last, lastMessage. role == . user {
173+ var updatedContent = lastMessage. content
174+ updatedContent. append ( . text( wrappedText) )
175+ result [ result. count - 1 ] = Message ( role: . user, content: updatedContent)
176+ } else {
177+ result. append ( . user( wrappedText) )
178+ }
179+
180+ result. append ( . assistant( " Noted background results. " ) )
181+
182+ print ( " [background] \( ANSIColor . dim) \( notifications. count) result(s) injected \( ANSIColor . reset) " )
183+ return result
184+ }
185+
151186 public static func buildSystemPrompt( cwd: String , skillDescriptions: String = " " ) -> String {
152187 var prompt = """
153188 You are a coding agent at \( cwd) . Use tools to solve tasks. \
@@ -158,6 +193,8 @@ public final class Agent {
158193 - Use the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.
159194 - Use task tools for persistent multi-step work with dependencies. \
160195 Tasks survive context compaction and process restarts.
196+ - Use background_run for long-running commands (builds, tests, installs). \
197+ Check with background_check.
161198 """
162199
163200 if !skillDescriptions. isEmpty {
@@ -386,6 +423,37 @@ extension Agent {
386423 ] ) ,
387424 " required " : . array( [ " task_id " ] )
388425 ] )
426+ ) ,
427+ ToolDefinition (
428+ name: " background_run " ,
429+ description: """
430+ Run a command in the background. Returns job_id immediately. \
431+ Use for long-running commands (builds, tests, installs).
432+ """ ,
433+ inputSchema: . object( [
434+ " type " : " object " ,
435+ " properties " : . object( [
436+ " command " : . object( [
437+ " type " : " string " ,
438+ " description " : " The shell command to execute in the background "
439+ ] )
440+ ] ) ,
441+ " required " : . array( [ " command " ] )
442+ ] )
443+ ) ,
444+ ToolDefinition (
445+ name: " background_check " ,
446+ description: " Check background job status. Omit job_id to list all. " ,
447+ inputSchema: . object( [
448+ " type " : " object " ,
449+ " properties " : . object( [
450+ " job_id " : . object( [
451+ " type " : " string " ,
452+ " description " : " The job ID to check "
453+ ] )
454+ ] ) ,
455+ " required " : . array( [ ] )
456+ ] )
389457 )
390458 ]
391459
@@ -402,7 +470,9 @@ extension Agent {
402470 " task_create " : executeTaskCreate,
403471 " task_update " : executeTaskUpdate,
404472 " task_list " : executeTaskList,
405- " task_get " : executeTaskGet
473+ " task_get " : executeTaskGet,
474+ " background_run " : executeBackgroundRun,
475+ " background_check " : executeBackgroundCheck
406476 ]
407477
408478 guard let handler = handlers [ name] else {
@@ -422,6 +492,8 @@ extension Agent {
422492 do {
423493 let result = try await shellExecutor. execute ( command)
424494 return . success( result. formatted)
495+ } catch ShellExecutorError . blockedCommand( let pattern) {
496+ return . failure( . executionFailed( " Dangerous command blocked (matched ' \( pattern) ') " ) )
425497 } catch {
426498 return . failure( . executionFailed( " \( error) " ) )
427499 }
@@ -444,8 +516,7 @@ extension Agent {
444516
445517 if let limit = input [ " limit " ] ? . intValue, limit < lines. count {
446518 output =
447- lines. prefix ( limit) . joined ( separator: " \n " )
448- + " \n ... ( \( lines. count - limit) more lines) "
519+ lines. prefix ( limit) . joined ( separator: " \n " ) + " \n ... ( \( lines. count - limit) more lines) "
449520 } else {
450521 output = text
451522 }
@@ -641,6 +712,21 @@ extension Agent {
641712 }
642713 }
643714
715+ private func executeBackgroundRun( _ input: JSONValue ) async -> Result < String , ToolError > {
716+ guard let command = input [ " command " ] ? . stringValue else {
717+ return . failure( . missingParameter( " command " ) )
718+ }
719+
720+ let confirmation = await backgroundManager. run ( command: command)
721+ return . success( confirmation)
722+ }
723+
724+ private func executeBackgroundCheck( _ input: JSONValue ) async -> Result < String , ToolError > {
725+ let jobId = input [ " job_id " ] ? . stringValue
726+ let result = await backgroundManager. check ( jobId: jobId)
727+ return . success( result)
728+ }
729+
644730 // MARK: Helpers
645731
646732 struct ToolProcessingResult {
@@ -729,25 +815,33 @@ extension Agent {
729815// MARK: - Configuration
730816
731817extension Agent {
732- fileprivate struct LoopConfig {
818+ struct LoopConfig {
733819 let tools : [ ToolDefinition ]
734820 let maxIterations : Int
735821 let enableNag : Bool
822+ let drainBackground : Bool
736823 let label : String
737824
738825 static let `default` = LoopConfig (
739826 tools: Agent . toolDefinitions,
740827 maxIterations: . max,
741828 enableNag: true ,
829+ drainBackground: true ,
742830 label: " agent "
743831 )
744832
833+ static let subagentExcludedTools : Set < String > = [
834+ " agent " , " todo " , " compact " , " task_create " , " task_update " ,
835+ " background_run " , " background_check "
836+ ]
837+
745838 static let subagent = LoopConfig (
746839 tools: Agent . toolDefinitions. filter {
747- !Set ( [ " agent " , " todo " , " compact " , " task_create " , " task_update " ] ) . contains ( $0. name)
840+ !subagentExcludedTools . contains ( $0. name)
748841 } ,
749842 maxIterations: 30 ,
750843 enableNag: false ,
844+ drainBackground: false ,
751845 label: " subagent "
752846 )
753847 }
0 commit comments