Skip to content

Commit 849cf04

Browse files
authored
Merge pull request docker#1330 from dgageot/todo
Single tool call to update multiple TODOs
2 parents e627269 + 0c2c265 commit 849cf04

File tree

4 files changed

+119
-51
lines changed

4 files changed

+119
-51
lines changed

pkg/acp/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ func isTodoTool(toolName string) bool {
698698
return slices.Contains([]string{
699699
builtin.ToolNameCreateTodo,
700700
builtin.ToolNameCreateTodos,
701-
builtin.ToolNameUpdateTodo,
701+
builtin.ToolNameUpdateTodos,
702702
builtin.ToolNameListTodos,
703703
}, toolName)
704704
}

pkg/tools/builtin/todo.go

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
const (
1414
ToolNameCreateTodo = "create_todo"
1515
ToolNameCreateTodos = "create_todos"
16-
ToolNameUpdateTodo = "update_todo"
16+
ToolNameUpdateTodos = "update_todos"
1717
ToolNameListTodos = "list_todos"
1818
)
1919

@@ -39,11 +39,15 @@ type CreateTodosArgs struct {
3939
Descriptions []string `json:"descriptions" jsonschema:"Descriptions of the todo items"`
4040
}
4141

42-
type UpdateTodoArgs struct {
42+
type TodoUpdate struct {
4343
ID string `json:"id" jsonschema:"ID of the todo item"`
4444
Status string `json:"status" jsonschema:"New status (pending, in-progress,completed)"`
4545
}
4646

47+
type UpdateTodosArgs struct {
48+
Updates []TodoUpdate `json:"updates" jsonschema:"List of todo updates"`
49+
}
50+
4751
type todoHandler struct {
4852
todos *concurrent.Slice[Todo]
4953
}
@@ -120,19 +124,41 @@ func (h *todoHandler) createTodos(_ context.Context, params CreateTodosArgs) (*t
120124
}, nil
121125
}
122126

123-
func (h *todoHandler) updateTodo(_ context.Context, params UpdateTodoArgs) (*tools.ToolCallResult, error) {
124-
_, idx := h.todos.Find(func(t Todo) bool { return t.ID == params.ID })
125-
if idx == -1 {
126-
return tools.ResultError(fmt.Sprintf("todo [%s] not found", params.ID)), nil
127+
func (h *todoHandler) updateTodos(_ context.Context, params UpdateTodosArgs) (*tools.ToolCallResult, error) {
128+
var notFound []string
129+
var updated []string
130+
131+
for _, update := range params.Updates {
132+
_, idx := h.todos.Find(func(t Todo) bool { return t.ID == update.ID })
133+
if idx == -1 {
134+
notFound = append(notFound, update.ID)
135+
continue
136+
}
137+
138+
h.todos.Update(idx, func(t Todo) Todo {
139+
t.Status = update.Status
140+
return t
141+
})
142+
updated = append(updated, fmt.Sprintf("%s -> %s", update.ID, update.Status))
127143
}
128144

129-
h.todos.Update(idx, func(t Todo) Todo {
130-
t.Status = params.Status
131-
return t
132-
})
145+
var output strings.Builder
146+
if len(updated) > 0 {
147+
fmt.Fprintf(&output, "Updated %d todos: %s", len(updated), strings.Join(updated, ", "))
148+
}
149+
if len(notFound) > 0 {
150+
if output.Len() > 0 {
151+
output.WriteString("; ")
152+
}
153+
fmt.Fprintf(&output, "Not found: %s", strings.Join(notFound, ", "))
154+
}
155+
156+
if len(notFound) > 0 && len(updated) == 0 {
157+
return tools.ResultError(output.String()), nil
158+
}
133159

134160
return &tools.ToolCallResult{
135-
Output: fmt.Sprintf("Updated todo [%s] to status: [%s]", params.ID, params.Status),
161+
Output: output.String(),
136162
Meta: h.todos.All(),
137163
}, nil
138164
}
@@ -179,14 +205,14 @@ func (t *TodoTool) Tools(context.Context) ([]tools.Tool, error) {
179205
},
180206
},
181207
{
182-
Name: ToolNameUpdateTodo,
208+
Name: ToolNameUpdateTodos,
183209
Category: "todo",
184-
Description: "Update the status of a todo item",
185-
Parameters: tools.MustSchemaFor[UpdateTodoArgs](),
210+
Description: "Update the status of one or more todo item(s)",
211+
Parameters: tools.MustSchemaFor[UpdateTodosArgs](),
186212
OutputSchema: tools.MustSchemaFor[string](),
187-
Handler: NewHandler(t.handler.updateTodo),
213+
Handler: NewHandler(t.handler.updateTodos),
188214
Annotations: tools.ToolAnnotations{
189-
Title: "Update TODO",
215+
Title: "Update TODOs",
190216
ReadOnlyHint: true, // Technically not read-only but has practically no destructive side effects.
191217
},
192218
},

pkg/tools/builtin/todo_test.go

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -93,34 +93,6 @@ func TestTodoTool_CreateTodos(t *testing.T) {
9393
require.Len(t, metaTodos, 4)
9494
}
9595

96-
func TestTodoTool_UpdateTodo(t *testing.T) {
97-
tool := NewTodoTool()
98-
99-
_, err := tool.handler.createTodo(t.Context(), CreateTodoArgs{
100-
Description: "Test todo item",
101-
})
102-
require.NoError(t, err)
103-
104-
result, err := tool.handler.updateTodo(t.Context(), UpdateTodoArgs{
105-
ID: "todo_1",
106-
Status: "completed",
107-
})
108-
require.NoError(t, err)
109-
assert.Contains(t, result.Output, "Updated todo [todo_1] to status: [completed]")
110-
111-
todos := tool.handler.todos.All()
112-
require.Len(t, todos, 1)
113-
assert.Equal(t, "completed", todos[0].Status)
114-
115-
// Verify Meta contains all todos with updated status
116-
metaTodos, ok := result.Meta.([]Todo)
117-
require.True(t, ok, "Meta should be []Todo")
118-
require.Len(t, metaTodos, 1)
119-
assert.Equal(t, "todo_1", metaTodos[0].ID)
120-
assert.Equal(t, "Test todo item", metaTodos[0].Description)
121-
assert.Equal(t, "completed", metaTodos[0].Status)
122-
}
123-
12496
func TestTodoTool_ListTodos(t *testing.T) {
12597
tool := NewTodoTool()
12698

@@ -153,15 +125,85 @@ func TestTodoTool_ListTodos(t *testing.T) {
153125
require.Len(t, metaTodos, 3)
154126
}
155127

156-
func TestTodoTool_UpdateNonexistentTodo(t *testing.T) {
128+
func TestTodoTool_UpdateTodos(t *testing.T) {
129+
tool := NewTodoTool()
130+
131+
// Create multiple todos first
132+
_, err := tool.handler.createTodos(t.Context(), CreateTodosArgs{
133+
Descriptions: []string{
134+
"First todo item",
135+
"Second todo item",
136+
"Third todo item",
137+
},
138+
})
139+
require.NoError(t, err)
140+
141+
// Update multiple todos in one call
142+
result, err := tool.handler.updateTodos(t.Context(), UpdateTodosArgs{
143+
Updates: []TodoUpdate{
144+
{ID: "todo_1", Status: "completed"},
145+
{ID: "todo_3", Status: "in-progress"},
146+
},
147+
})
148+
require.NoError(t, err)
149+
assert.False(t, result.IsError)
150+
assert.Contains(t, result.Output, "Updated 2 todos")
151+
assert.Contains(t, result.Output, "todo_1 -> completed")
152+
assert.Contains(t, result.Output, "todo_3 -> in-progress")
153+
154+
// Verify the todos were updated
155+
todos := tool.handler.todos.All()
156+
require.Len(t, todos, 3)
157+
assert.Equal(t, "completed", todos[0].Status)
158+
assert.Equal(t, "pending", todos[1].Status)
159+
assert.Equal(t, "in-progress", todos[2].Status)
160+
161+
// Verify Meta contains all todos with updated status
162+
metaTodos, ok := result.Meta.([]Todo)
163+
require.True(t, ok, "Meta should be []Todo")
164+
require.Len(t, metaTodos, 3)
165+
}
166+
167+
func TestTodoTool_UpdateTodos_PartialFailure(t *testing.T) {
157168
tool := NewTodoTool()
158169

159-
res, err := tool.handler.updateTodo(t.Context(), UpdateTodoArgs{
160-
ID: "nonexistent_todo",
161-
Status: "completed",
170+
// Create a single todo
171+
_, err := tool.handler.createTodo(t.Context(), CreateTodoArgs{
172+
Description: "Test todo item",
173+
})
174+
require.NoError(t, err)
175+
176+
// Try to update one existing and one non-existing todo
177+
result, err := tool.handler.updateTodos(t.Context(), UpdateTodosArgs{
178+
Updates: []TodoUpdate{
179+
{ID: "todo_1", Status: "completed"},
180+
{ID: "nonexistent", Status: "completed"},
181+
},
182+
})
183+
require.NoError(t, err)
184+
assert.False(t, result.IsError) // Not an error because at least one succeeded
185+
assert.Contains(t, result.Output, "Updated 1 todos")
186+
assert.Contains(t, result.Output, "Not found: nonexistent")
187+
188+
// Verify the existing todo was updated
189+
todos := tool.handler.todos.All()
190+
require.Len(t, todos, 1)
191+
assert.Equal(t, "completed", todos[0].Status)
192+
}
193+
194+
func TestTodoTool_UpdateTodos_AllNotFound(t *testing.T) {
195+
tool := NewTodoTool()
196+
197+
// Try to update non-existing todos
198+
result, err := tool.handler.updateTodos(t.Context(), UpdateTodosArgs{
199+
Updates: []TodoUpdate{
200+
{ID: "nonexistent1", Status: "completed"},
201+
{ID: "nonexistent2", Status: "completed"},
202+
},
162203
})
163204
require.NoError(t, err)
164-
require.True(t, res.IsError)
205+
assert.True(t, result.IsError) // Error because all failed
206+
assert.Contains(t, result.Output, "Not found: nonexistent1, nonexistent2")
165207
}
166208

167209
func TestTodoTool_OutputSchema(t *testing.T) {

pkg/tui/components/tool/factory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func newDefaultRegistry() *Registry {
8383
[]string{
8484
builtin.ToolNameCreateTodo,
8585
builtin.ToolNameCreateTodos,
86-
builtin.ToolNameUpdateTodo,
86+
builtin.ToolNameUpdateTodos,
8787
builtin.ToolNameListTodos,
8888
},
8989
todotool.New,

0 commit comments

Comments
 (0)