11import Foundation
22
33public final class Agent {
4- public static let version = " 0.2.0 "
4+ public static let version = " 0.3.0 "
5+
6+ private static let todoReminderThreshold = 3
57
68 private let apiClient : any APIClientProtocol
79 private let model : String
810 private let systemPrompt : String
911 private let workingDirectory : String
12+
1013 private let shellExecutor : ShellExecutor
11- private var messages : [ Message ] = [ ]
14+ private let todoManager = TodoManager ( )
1215
1316 public init (
1417 apiClient: APIClientProtocol ,
@@ -26,7 +29,8 @@ public final class Agent {
2629 // MARK: - Agent loop
2730
2831 public func run( query: String ) async throws -> String {
29- messages. append ( . user( query) )
32+ var messages : [ Message ] = [ . user( query) ]
33+ var turnsWithoutTodo = 0
3034
3135 while true {
3236 let request = APIRequest (
@@ -51,11 +55,17 @@ public final class Agent {
5155 }
5256
5357 var results : [ ContentBlock ] = [ ]
58+ var didUseTodo = false
59+
5460 for block in response. content {
5561 if case . toolUse( let id, let name, let input) = block {
5662 printToolCall ( name: name, input: input)
5763 let toolResult = await executeTool ( name: name, input: input)
5864
65+ if name == " todo " {
66+ didUseTodo = true
67+ }
68+
5969 switch toolResult {
6070 case . success( let output) :
6171 print ( " \( ANSIColor . dim) \( String ( output. prefix ( 200 ) ) ) \( ANSIColor . reset) " )
@@ -68,6 +78,11 @@ public final class Agent {
6878 }
6979 }
7080
81+ turnsWithoutTodo = didUseTodo ? 0 : turnsWithoutTodo + 1
82+ if turnsWithoutTodo >= Self . todoReminderThreshold && todoManager. hasOpenItems ( ) {
83+ results. append ( . text( " Update your todos. " ) )
84+ }
85+
7186 messages. append ( Message ( role: . user, content: results) )
7287 }
7388 }
@@ -79,6 +94,7 @@ public final class Agent {
7994
8095 - Prefer read_file/write_file/edit_file over bash for file operations
8196 - Always check tool results before proceeding
97+ - Use the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.
8298 """
8399 }
84100}
@@ -164,6 +180,31 @@ extension Agent {
164180 ] ) ,
165181 " required " : . array( [ " path " , " old_text " , " new_text " ] )
166182 ] )
183+ ) ,
184+ ToolDefinition (
185+ name: " todo " ,
186+ description: " Update task list. Track progress on multi-step tasks. " ,
187+ inputSchema: . object( [
188+ " type " : " object " ,
189+ " properties " : . object( [
190+ " items " : . object( [
191+ " type " : " array " ,
192+ " items " : . object( [
193+ " type " : " object " ,
194+ " properties " : . object( [
195+ " id " : . object( [ " type " : " string " ] ) ,
196+ " text " : . object( [ " type " : " string " ] ) ,
197+ " status " : . object( [
198+ " type " : " string " ,
199+ " enum " : . array( [ " pending " , " in_progress " , " completed " ] )
200+ ] )
201+ ] ) ,
202+ " required " : . array( [ " id " , " text " , " status " ] )
203+ ] )
204+ ] )
205+ ] ) ,
206+ " required " : . array( [ " items " ] )
207+ ] )
167208 )
168209 ]
169210
@@ -172,7 +213,8 @@ extension Agent {
172213 " bash " : executeBash,
173214 " read_file " : executeReadFile,
174215 " write_file " : executeWriteFile,
175- " edit_file " : executeEditFile
216+ " edit_file " : executeEditFile,
217+ " todo " : executeTodo
176218 ]
177219
178220 guard let handler = handlers [ name] else {
@@ -294,6 +336,36 @@ extension Agent {
294336 }
295337 }
296338
339+ private func executeTodo( _ input: JSONValue ) async -> Result < String , ToolError > {
340+ guard let itemsArray = input [ " items " ] ? . arrayValue else {
341+ return . failure( . missingParameter( " items " ) )
342+ }
343+
344+ var todoItems : [ TodoItem ] = [ ]
345+ for element in itemsArray {
346+ guard let id = element [ " id " ] ? . stringValue else {
347+ return . failure( . missingParameter( " items[].id " ) )
348+ }
349+ guard let text = element [ " text " ] ? . stringValue else {
350+ return . failure( . missingParameter( " items[].text " ) )
351+ }
352+ guard let statusString = element [ " status " ] ? . stringValue else {
353+ return . failure( . missingParameter( " items[].status " ) )
354+ }
355+ guard let status = TodoStatus ( rawValue: statusString) else {
356+ return . failure( . executionFailed( " Invalid status ' \( statusString) ' for item \( id) " ) )
357+ }
358+ todoItems. append ( TodoItem ( id: id, text: text, status: status) )
359+ }
360+
361+ do {
362+ try todoManager. update ( items: todoItems)
363+ return . success( todoManager. render ( ) )
364+ } catch {
365+ return . failure( . executionFailed( " \( error) " ) )
366+ }
367+ }
368+
297369 // MARK: Helpers
298370
299371 private func resolveSafePath( _ relativePath: String ) -> Result < String , ToolError > {
0 commit comments