Skip to content

Commit 6416e3b

Browse files
author
Gordon
committed
todo: include full state and reminder in all tool responses
Add AllTodos field to CreateTodoOutput, CreateTodosOutput, and UpdateTodosOutput so every response includes the complete current state of all todo items. This gives the LLM full visibility into the todo list without needing a separate list_todos call. Also removes the auto-clear-on-all-completed behavior so that completed items remain visible, and adds CreateTodoOutput as a dedicated output type for create_todo (replacing bare Todo).
1 parent 9c1cab4 commit 6416e3b

2 files changed

Lines changed: 105 additions & 30 deletions

File tree

pkg/tools/builtin/todo.go

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,22 @@ type UpdateTodosArgs struct {
5454

5555
// Output types for JSON-structured responses.
5656

57+
type CreateTodoOutput struct {
58+
Created Todo `json:"created" jsonschema:"The created todo item"`
59+
AllTodos []Todo `json:"all_todos" jsonschema:"Current state of all todo items"`
60+
Reminder string `json:"reminder,omitempty" jsonschema:"Reminder about incomplete todos that still need to be completed"`
61+
}
62+
5763
type CreateTodosOutput struct {
58-
Created []Todo `json:"created" jsonschema:"List of created todo items"`
64+
Created []Todo `json:"created" jsonschema:"List of created todo items"`
65+
AllTodos []Todo `json:"all_todos" jsonschema:"Current state of all todo items"`
66+
Reminder string `json:"reminder,omitempty" jsonschema:"Reminder about incomplete todos that still need to be completed"`
5967
}
6068

6169
type UpdateTodosOutput struct {
6270
Updated []TodoUpdate `json:"updated,omitempty" jsonschema:"List of successfully updated todos"`
6371
NotFound []string `json:"not_found,omitempty" jsonschema:"IDs of todos that were not found"`
72+
AllTodos []Todo `json:"all_todos" jsonschema:"Current state of all todo items"`
6473
Reminder string `json:"reminder,omitempty" jsonschema:"Reminder about incomplete todos that still need to be completed"`
6574
}
6675

@@ -202,15 +211,24 @@ func (h *todoHandler) jsonResult(v any) (*tools.ToolCallResult, error) {
202211
}
203212

204213
func (h *todoHandler) createTodo(_ context.Context, params CreateTodoArgs) (*tools.ToolCallResult, error) {
205-
return h.jsonResult(h.addTodo(params.Description))
214+
created := h.addTodo(params.Description)
215+
return h.jsonResult(CreateTodoOutput{
216+
Created: created,
217+
AllTodos: h.storage.All(),
218+
Reminder: h.incompleteReminder(),
219+
})
206220
}
207221

208222
func (h *todoHandler) createTodos(_ context.Context, params CreateTodosArgs) (*tools.ToolCallResult, error) {
209223
created := make([]Todo, 0, len(params.Descriptions))
210224
for _, desc := range params.Descriptions {
211225
created = append(created, h.addTodo(desc))
212226
}
213-
return h.jsonResult(CreateTodosOutput{Created: created})
227+
return h.jsonResult(CreateTodosOutput{
228+
Created: created,
229+
AllTodos: h.storage.All(),
230+
Reminder: h.incompleteReminder(),
231+
})
214232
}
215233

216234
func (h *todoHandler) updateTodos(_ context.Context, params UpdateTodosArgs) (*tools.ToolCallResult, error) {
@@ -239,28 +257,12 @@ func (h *todoHandler) updateTodos(_ context.Context, params UpdateTodosArgs) (*t
239257
return res, nil
240258
}
241259

242-
if h.allCompleted() {
243-
h.storage.Clear()
244-
} else {
245-
result.Reminder = h.incompleteReminder()
246-
}
260+
result.AllTodos = h.storage.All()
261+
result.Reminder = h.incompleteReminder()
247262

248263
return h.jsonResult(result)
249264
}
250265

251-
func (h *todoHandler) allCompleted() bool {
252-
all := h.storage.All()
253-
if len(all) == 0 {
254-
return false
255-
}
256-
for _, todo := range all {
257-
if todo.Status != "completed" {
258-
return false
259-
}
260-
}
261-
return true
262-
}
263-
264266
// incompleteReminder returns a reminder string listing any non-completed todos,
265267
// or an empty string if all are completed (or storage is empty).
266268
func (h *todoHandler) incompleteReminder() string {
@@ -306,7 +308,7 @@ func (t *TodoTool) Tools(context.Context) ([]tools.Tool, error) {
306308
Category: "todo",
307309
Description: "Create a new todo item with a description",
308310
Parameters: tools.MustSchemaFor[CreateTodoArgs](),
309-
OutputSchema: tools.MustSchemaFor[Todo](),
311+
OutputSchema: tools.MustSchemaFor[CreateTodoOutput](),
310312
Handler: tools.NewHandler(t.handler.createTodo),
311313
Annotations: tools.ToolAnnotations{
312314
Title: "Create TODO",

pkg/tools/builtin/todo_test.go

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@ func TestTodoTool_CreateTodo(t *testing.T) {
3131
})
3232
require.NoError(t, err)
3333

34-
var output Todo
34+
var output CreateTodoOutput
3535
require.NoError(t, json.Unmarshal([]byte(result.Output), &output))
36-
assert.Equal(t, "todo_1", output.ID)
37-
assert.Equal(t, "Test todo item", output.Description)
38-
assert.Equal(t, "pending", output.Status)
36+
assert.Equal(t, "todo_1", output.Created.ID)
37+
assert.Equal(t, "Test todo item", output.Created.Description)
38+
assert.Equal(t, "pending", output.Created.Status)
39+
40+
// Full state is included in the response
41+
require.Len(t, output.AllTodos, 1)
42+
assert.Equal(t, "todo_1", output.AllTodos[0].ID)
43+
assert.Contains(t, output.Reminder, "todo_1")
3944

4045
require.Equal(t, 1, storage.Len())
4146
requireMeta(t, result, 1)
@@ -59,10 +64,16 @@ func TestTodoTool_CreateTodos(t *testing.T) {
5964
assert.Equal(t, "todo_2", output.Created[1].ID)
6065
assert.Equal(t, "todo_3", output.Created[2].ID)
6166

67+
// Full state included in response
68+
require.Len(t, output.AllTodos, 3)
69+
assert.Contains(t, output.Reminder, "todo_1")
70+
assert.Contains(t, output.Reminder, "todo_2")
71+
assert.Contains(t, output.Reminder, "todo_3")
72+
6273
assert.Equal(t, 3, storage.Len())
6374
requireMeta(t, result, 3)
6475

65-
// A second call continues the ID sequence
76+
// A second call continues the ID sequence and includes all 4 items
6677
result, err = tool.handler.createTodos(t.Context(), CreateTodosArgs{
6778
Descriptions: []string{"Last"},
6879
})
@@ -71,6 +82,7 @@ func TestTodoTool_CreateTodos(t *testing.T) {
7182
require.NoError(t, json.Unmarshal([]byte(result.Output), &output))
7283
require.Len(t, output.Created, 1)
7384
assert.Equal(t, "todo_4", output.Created[0].ID)
85+
require.Len(t, output.AllTodos, 4)
7486
assert.Equal(t, 4, storage.Len())
7587
requireMeta(t, result, 4)
7688
}
@@ -144,6 +156,12 @@ func TestTodoTool_UpdateTodos(t *testing.T) {
144156
assert.Equal(t, "in-progress", output.Updated[1].Status)
145157
assert.Empty(t, output.NotFound)
146158

159+
// Full state included in response
160+
require.Len(t, output.AllTodos, 3)
161+
assert.Equal(t, "completed", output.AllTodos[0].Status)
162+
assert.Equal(t, "pending", output.AllTodos[1].Status)
163+
assert.Equal(t, "in-progress", output.AllTodos[2].Status)
164+
147165
// Reminder should list incomplete todos
148166
assert.Contains(t, output.Reminder, "todo_2")
149167
assert.Contains(t, output.Reminder, "todo_3")
@@ -212,7 +230,7 @@ func TestTodoTool_UpdateTodos_AllNotFound(t *testing.T) {
212230
assert.Equal(t, "nonexistent2", output.NotFound[1])
213231
}
214232

215-
func TestTodoTool_UpdateTodos_ClearsWhenAllCompleted(t *testing.T) {
233+
func TestTodoTool_UpdateTodos_AllCompleted_NoAutoRemoval(t *testing.T) {
216234
storage := NewMemoryTodoStorage()
217235
tool := NewTodoTool(WithStorage(storage))
218236

@@ -234,8 +252,14 @@ func TestTodoTool_UpdateTodos_ClearsWhenAllCompleted(t *testing.T) {
234252
require.Len(t, output.Updated, 2)
235253
assert.Empty(t, output.Reminder) // no reminder when all completed
236254

237-
assert.Empty(t, storage.All())
238-
requireMeta(t, result, 0)
255+
// Full state shows both items as completed
256+
require.Len(t, output.AllTodos, 2)
257+
assert.Equal(t, "completed", output.AllTodos[0].Status)
258+
assert.Equal(t, "completed", output.AllTodos[1].Status)
259+
260+
// Todos remain in storage (no auto-clear on completion)
261+
assert.Equal(t, 2, storage.Len())
262+
requireMeta(t, result, 2)
239263
}
240264

241265
func TestTodoTool_WithStorage(t *testing.T) {
@@ -282,6 +306,55 @@ func TestTodoTool_ParametersAreObjects(t *testing.T) {
282306
}
283307
}
284308

309+
func TestTodoTool_CreateTodo_FullStateOutput(t *testing.T) {
310+
tool := NewTodoTool()
311+
312+
// Create first todo
313+
result1, err := tool.handler.createTodo(t.Context(), CreateTodoArgs{Description: "First"})
314+
require.NoError(t, err)
315+
var out1 CreateTodoOutput
316+
require.NoError(t, json.Unmarshal([]byte(result1.Output), &out1))
317+
require.Len(t, out1.AllTodos, 1)
318+
assert.Contains(t, out1.Reminder, "todo_1")
319+
320+
// Create second todo — response shows both
321+
result2, err := tool.handler.createTodo(t.Context(), CreateTodoArgs{Description: "Second"})
322+
require.NoError(t, err)
323+
var out2 CreateTodoOutput
324+
require.NoError(t, json.Unmarshal([]byte(result2.Output), &out2))
325+
require.Len(t, out2.AllTodos, 2)
326+
assert.Contains(t, out2.Reminder, "todo_1")
327+
assert.Contains(t, out2.Reminder, "todo_2")
328+
}
329+
330+
func TestTodoTool_UpdateTodos_FullStateOutput(t *testing.T) {
331+
tool := NewTodoTool()
332+
333+
_, err := tool.handler.createTodos(t.Context(), CreateTodosArgs{
334+
Descriptions: []string{"A", "B", "C"},
335+
})
336+
require.NoError(t, err)
337+
338+
result, err := tool.handler.updateTodos(t.Context(), UpdateTodosArgs{
339+
Updates: []TodoUpdate{{ID: "todo_1", Status: "completed"}},
340+
})
341+
require.NoError(t, err)
342+
343+
var output UpdateTodosOutput
344+
require.NoError(t, json.Unmarshal([]byte(result.Output), &output))
345+
346+
// AllTodos shows full state including the completed item
347+
require.Len(t, output.AllTodos, 3)
348+
assert.Equal(t, "completed", output.AllTodos[0].Status)
349+
assert.Equal(t, "pending", output.AllTodos[1].Status)
350+
assert.Equal(t, "pending", output.AllTodos[2].Status)
351+
352+
// Reminder only lists incomplete items
353+
assert.NotContains(t, output.Reminder, "todo_1")
354+
assert.Contains(t, output.Reminder, "todo_2")
355+
assert.Contains(t, output.Reminder, "todo_3")
356+
}
357+
285358
// requireMeta asserts that result.Meta is a []Todo of the expected length.
286359
func requireMeta(t *testing.T, result *tools.ToolCallResult, expectedLen int) {
287360
t.Helper()

0 commit comments

Comments
 (0)