Files
Ettore Di Giacinto f195b2553f feat(todo): add todo-based goal execution similar to cursor agent mode (#25)
* feat(todo): add todo-based goal execution similar to cursor agent mode

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* reinforce tests

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Bump timeout for tests

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat: guided tools

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-01-15 21:19:15 +01:00

187 lines
4.6 KiB
Go

package structures
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/sashabaranov/go-openai/jsonschema"
)
type TODO struct {
ID string `json:"id"`
Description string `json:"description"`
Completed bool `json:"completed"`
Feedback string `json:"feedback,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
type TODOList struct {
TODOs []TODO `json:"todos"`
Markdown string `json:"markdown"`
LastUpdated time.Time `json:"last_updated"`
}
func StructureTODO() (Structure, *TODOList) {
return structureType[TODOList](jsonschema.Definition{
Type: jsonschema.Object,
AdditionalProperties: false,
Properties: map[string]jsonschema.Definition{
"todos": {
Type: jsonschema.Array,
Description: "List of TODO items",
Items: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"id": {
Type: jsonschema.String,
Description: "Unique identifier for the TODO",
},
"description": {
Type: jsonschema.String,
Description: "Description of the TODO item",
},
"completed": {
Type: jsonschema.Boolean,
Description: "Whether the TODO is completed",
},
"feedback": {
Type: jsonschema.String,
Description: "Optional feedback for this TODO",
},
"metadata": {
Type: jsonschema.Object,
Description: "Optional metadata for this TODO",
},
},
Required: []string{"id", "description", "completed"},
},
},
},
Required: []string{"todos"},
})
}
// ToMarkdown generates markdown representation with checkboxes
func (tl *TODOList) ToMarkdown() string {
var builder strings.Builder
for _, todo := range tl.TODOs {
if todo.Completed {
builder.WriteString(fmt.Sprintf("- [x] %s\n", todo.Description))
} else {
builder.WriteString(fmt.Sprintf("- [ ] %s\n", todo.Description))
}
if todo.Feedback != "" {
builder.WriteString(fmt.Sprintf(" Feedback: %s\n", todo.Feedback))
}
}
tl.Markdown = builder.String()
return tl.Markdown
}
// FromMarkdown parses markdown checkboxes into TODO list
func (tl *TODOList) FromMarkdown(markdown string) error {
lines := strings.Split(markdown, "\n")
tl.TODOs = []TODO{}
for i, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Check for checkbox pattern: - [x] or - [ ]
if strings.HasPrefix(line, "- [") {
completed := strings.Contains(line, "- [x]") || strings.Contains(line, "- [X]")
description := ""
if completed {
description = strings.TrimPrefix(line, "- [x]")
description = strings.TrimPrefix(description, "- [X]")
} else {
description = strings.TrimPrefix(line, "- [ ]")
}
description = strings.TrimSpace(description)
// Check if next line is feedback
feedback := ""
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if strings.HasPrefix(nextLine, "Feedback:") {
feedback = strings.TrimPrefix(nextLine, "Feedback:")
feedback = strings.TrimSpace(feedback)
}
}
todo := TODO{
ID: fmt.Sprintf("%d", len(tl.TODOs)+1),
Description: description,
Completed: completed,
Feedback: feedback,
}
tl.TODOs = append(tl.TODOs, todo)
}
}
tl.Markdown = markdown
tl.LastUpdated = time.Now()
return nil
}
// ToJSON serializes TODOList to JSON
func (tl *TODOList) ToJSON() ([]byte, error) {
return json.Marshal(tl)
}
// FromJSON deserializes JSON to TODOList
func (tl *TODOList) FromJSON(data []byte) error {
return json.Unmarshal(data, tl)
}
// MarkComplete marks a TODO as completed by ID
func (tl *TODOList) MarkComplete(id string) {
for i := range tl.TODOs {
if tl.TODOs[i].ID == id {
tl.TODOs[i].Completed = true
tl.LastUpdated = time.Now()
tl.ToMarkdown()
return
}
}
}
// AddFeedback adds feedback to a TODO by ID
func (tl *TODOList) AddFeedback(id, feedback string) {
for i := range tl.TODOs {
if tl.TODOs[i].ID == id {
tl.TODOs[i].Feedback = feedback
tl.LastUpdated = time.Now()
tl.ToMarkdown()
return
}
}
}
// GetIncompleteTODOs returns all incomplete TODOs
func (tl *TODOList) GetIncompleteTODOs() []TODO {
var incomplete []TODO
for _, todo := range tl.TODOs {
if !todo.Completed {
incomplete = append(incomplete, todo)
}
}
return incomplete
}
// GetCompletedTODOs returns all completed TODOs
func (tl *TODOList) GetCompletedTODOs() []TODO {
var completed []TODO
for _, todo := range tl.TODOs {
if todo.Completed {
completed = append(completed, todo)
}
}
return completed
}