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
14+ private let todoManager = TodoManager ( )
15+
1116 private var messages : [ Message ] = [ ]
1217
1318 public init (
@@ -27,6 +32,7 @@ public final class Agent {
2732
2833 public func run( query: String ) async throws -> String {
2934 messages. append ( . user( query) )
35+ var turnsWithoutTodo = 0
3036
3137 while true {
3238 let request = APIRequest (
@@ -51,11 +57,17 @@ public final class Agent {
5157 }
5258
5359 var results : [ ContentBlock ] = [ ]
60+ var didUseTodo = false
61+
5462 for block in response. content {
5563 if case . toolUse( let id, let name, let input) = block {
5664 printToolCall ( name: name, input: input)
5765 let toolResult = await executeTool ( name: name, input: input)
5866
67+ if name == " todo " {
68+ didUseTodo = true
69+ }
70+
5971 switch toolResult {
6072 case . success( let output) :
6173 print ( " \( ANSIColor . dim) \( String ( output. prefix ( 200 ) ) ) \( ANSIColor . reset) " )
@@ -68,6 +80,11 @@ public final class Agent {
6880 }
6981 }
7082
83+ turnsWithoutTodo = didUseTodo ? 0 : turnsWithoutTodo + 1
84+ if turnsWithoutTodo >= Self . todoReminderThreshold && todoManager. hasOpenItems ( ) {
85+ results. append ( . text( " Update your todos. " ) )
86+ }
87+
7188 messages. append ( Message ( role: . user, content: results) )
7289 }
7390 }
@@ -79,6 +96,7 @@ public final class Agent {
7996
8097 - Prefer read_file/write_file/edit_file over bash for file operations
8198 - Always check tool results before proceeding
99+ - Use the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.
82100 """
83101 }
84102}
@@ -164,6 +182,31 @@ extension Agent {
164182 ] ) ,
165183 " required " : . array( [ " path " , " old_text " , " new_text " ] )
166184 ] )
185+ ) ,
186+ ToolDefinition (
187+ name: " todo " ,
188+ description: " Update task list. Track progress on multi-step tasks. " ,
189+ inputSchema: . object( [
190+ " type " : " object " ,
191+ " properties " : . object( [
192+ " items " : . object( [
193+ " type " : " array " ,
194+ " items " : . object( [
195+ " type " : " object " ,
196+ " properties " : . object( [
197+ " id " : . object( [ " type " : " string " ] ) ,
198+ " text " : . object( [ " type " : " string " ] ) ,
199+ " status " : . object( [
200+ " type " : " string " ,
201+ " enum " : . array( [ " pending " , " in_progress " , " completed " ] )
202+ ] )
203+ ] ) ,
204+ " required " : . array( [ " id " , " text " , " status " ] )
205+ ] )
206+ ] )
207+ ] ) ,
208+ " required " : . array( [ " items " ] )
209+ ] )
167210 )
168211 ]
169212
@@ -172,7 +215,8 @@ extension Agent {
172215 " bash " : executeBash,
173216 " read_file " : executeReadFile,
174217 " write_file " : executeWriteFile,
175- " edit_file " : executeEditFile
218+ " edit_file " : executeEditFile,
219+ " todo " : executeTodo
176220 ]
177221
178222 guard let handler = handlers [ name] else {
@@ -294,6 +338,36 @@ extension Agent {
294338 }
295339 }
296340
341+ private func executeTodo( _ input: JSONValue ) async -> Result < String , ToolError > {
342+ guard let itemsArray = input [ " items " ] ? . arrayValue else {
343+ return . failure( . missingParameter( " items " ) )
344+ }
345+
346+ var todoItems : [ TodoItem ] = [ ]
347+ for element in itemsArray {
348+ guard let id = element [ " id " ] ? . stringValue else {
349+ return . failure( . missingParameter( " items[].id " ) )
350+ }
351+ guard let text = element [ " text " ] ? . stringValue else {
352+ return . failure( . missingParameter( " items[].text " ) )
353+ }
354+ guard let statusString = element [ " status " ] ? . stringValue else {
355+ return . failure( . missingParameter( " items[].status " ) )
356+ }
357+ guard let status = TodoStatus ( rawValue: statusString) else {
358+ return . failure( . executionFailed( " Invalid status ' \( statusString) ' for item \( id) " ) )
359+ }
360+ todoItems. append ( TodoItem ( id: id, text: text, status: status) )
361+ }
362+
363+ do {
364+ try todoManager. update ( items: todoItems)
365+ return . success( todoManager. render ( ) )
366+ } catch {
367+ return . failure( . executionFailed( " \( error) " ) )
368+ }
369+ }
370+
297371 // MARK: Helpers
298372
299373 private func resolveSafePath( _ relativePath: String ) -> Result < String , ToolError > {
0 commit comments