| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 1 | package loop |
| 2 | |
| 3 | import ( |
| 4 | _ "embed" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "sync" |
| 8 | |
| 9 | "dodo.cloud/neo/tools" |
| 10 | |
| 11 | "github.com/anthropics/anthropic-sdk-go" |
| 12 | "github.com/invopop/jsonschema" |
| 13 | ) |
| 14 | |
| 15 | //go:embed CONVERSATION_RULES.md |
| 16 | var systemPrompt string |
| 17 | |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 18 | var todo *ToDo |
| 19 | |
| 20 | func Run(pr PromptReader, client *Client, tools tools.Registry) error { |
| 21 | RegisterToDoTools(tools) |
| 22 | fmt.Printf("YOU: ") |
| 23 | prompt, err := pr.Read() |
| 24 | if err != nil { |
| 25 | return err |
| 26 | } |
| 27 | todo = &ToDo{} |
| 28 | todo.ID = "1" |
| 29 | todo.Title = prompt |
| 30 | todo.AssignedTo = "assistant" |
| 31 | agents := []Agent{ |
| 32 | &UserAgent{pr}, |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 33 | &AnthropicAgent{pr, tools, client.c}, |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 34 | } |
| 35 | var wg sync.WaitGroup |
| 36 | for _, a := range agents { |
| 37 | wg.Add(1) |
| 38 | go func() { |
| 39 | if err := a.Run(todo); err != nil { |
| 40 | panic(err) |
| 41 | } |
| 42 | wg.Done() |
| 43 | }() |
| 44 | } |
| 45 | wg.Wait() |
| 46 | return nil |
| 47 | } |
| 48 | |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 49 | func pickToDoItem(todo *ToDo) *ToDo { |
| 50 | if todo.Done { |
| 51 | return nil |
| 52 | } |
| 53 | for _, i := range todo.Items { |
| 54 | if ret := pickToDoItem(i); ret != nil { |
| 55 | return ret |
| 56 | } |
| 57 | } |
| 58 | return todo |
| 59 | } |
| 60 | |
| 61 | func findItemByID(todo *ToDo, id string) *ToDo { |
| 62 | if todo.ID == id { |
| 63 | return todo |
| 64 | } |
| 65 | for _, i := range todo.Items { |
| 66 | if ret := findItemByID(i, id); ret != nil { |
| 67 | return ret |
| 68 | } |
| 69 | } |
| 70 | return nil |
| 71 | } |
| 72 | |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 73 | type ToDoSubItem struct { |
| 74 | Title string `json:"title" jsonschema:"title=title,description=high level title of the TODO item,required"` |
| 75 | Description string `json:"description" jsonschema:"title=description,description=detailed description what this TODO item is about"` |
| 76 | AssignedTo string `json:"assignedTo" jsonschema:"title=assigned to,description=name of the person who shall work on this utem"` |
| 77 | } |
| 78 | |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 79 | type ToDoItem struct { |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 80 | ParentID string `json:"parentId" jsonschema:"title=parent item id,description=ID of the parent TODO item this one shall be added to as a sub-item"` |
| 81 | Title string `json:"title" jsonschema:"title=title,description=high level title of the TODO item,required"` |
| 82 | Description string `json:"description" jsonschema:"title=description,description=detailed description what this TODO item is about"` |
| 83 | AssignedTo string `json:"assignedTo" jsonschema:"title=assigned to,description=name of the person who shall work on this utem"` |
| 84 | Items []ToDoSubItem `json:"items" jsonschema:"title=sub items,description:array of sub-items"` |
| 85 | Parallel bool `json:"parallel" jsonschema:"title=parallel,description=if true sub-items may be worked on in parallel and there is no depencency between them, otherwise they shall be worked on sequentially"` |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 86 | } |
| 87 | |
| 88 | type ToDoAddItemArgs struct { |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 89 | Items []ToDoItem `json:"items" jsonschema:"title=items,description="items to add to the TODO list,required""` |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 90 | } |
| 91 | |
| 92 | func ToDoAddItem(args ToDoAddItemArgs) (string, error) { |
| 93 | for _, td := range args.Items { |
| 94 | item := findItemByID(todo, td.ParentID) |
| 95 | if item == nil { |
| 96 | return "error", fmt.Errorf("TODO item with given id not found: %s", td.ParentID) |
| 97 | } |
| 98 | id := fmt.Sprintf("%s.%d", item.ID, len(item.Items)+1) |
| 99 | item.Items = append(item.Items, &ToDo{ |
| 100 | ID: id, |
| 101 | Title: td.Title, |
| 102 | Description: td.Description, |
| 103 | AssignedTo: td.AssignedTo, |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 104 | Parallel: td.Parallel, |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 105 | }) |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 106 | ti := item.Items[len(item.Items)-1] |
| 107 | for _, s := range td.Items { |
| 108 | sid := fmt.Sprintf("%s.%d", id, len(ti.Items)+1) |
| 109 | ti.Items = append(ti.Items, &ToDo{ |
| 110 | ID: sid, |
| 111 | Title: s.Title, |
| 112 | Description: s.Description, |
| 113 | AssignedTo: s.AssignedTo, |
| 114 | }) |
| 115 | } |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 116 | } |
| 117 | return "done", nil |
| 118 | } |
| 119 | |
| 120 | type ToDoMarkItemDoneArgs struct { |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 121 | ID string `json:"id" jsonschema:"title=id,description=id of the TODO item to mark as DONE,required"` |
| 122 | Summary string `json:"summary" jsonschem:"title=summary,description=detailed summary of current item and all of it's sub-item trees.,required"` |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 123 | } |
| 124 | |
| 125 | func ToDoMarkItemDone(args ToDoMarkItemDoneArgs) (string, error) { |
| 126 | item := findItemByID(todo, args.ID) |
| 127 | if item == nil { |
| 128 | return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID) |
| 129 | } |
| 130 | item.Done = true |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 131 | item.Summary = args.Summary |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 132 | return "done", nil |
| 133 | } |
| 134 | |
| 135 | type ToDoItemAddCommentArgs struct { |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 136 | ID string `json:"id" jsonschema:"title=id,description=id of the TODO item to add comment to,required"` |
| 137 | Comment string `json:"comment" jsonschema:"title=comment,description=actual comment text,required"` |
| 138 | AssignTo string `json:"assignTo" jsonschema:"title=assigned to,description=name of the person who shall be assigned to TODO item with given ID, if empty assignment does not chage"` |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 139 | } |
| 140 | |
| 141 | func ToDoItemAddComment(args ToDoItemAddCommentArgs) (string, error) { |
| 142 | item := findItemByID(todo, args.ID) |
| 143 | if item == nil { |
| 144 | return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID) |
| 145 | } |
| 146 | if len(item.Discussion) == 0 { |
| 147 | return "error", fmt.Errorf("You shall never initiate a discussion, if you want to clarify something create a TODO item for it.") |
| 148 | } |
| 149 | item.Discussion = append(item.Discussion, Comment{ |
| 150 | Author: "assistant", |
| 151 | Comment: args.Comment, |
| 152 | }) |
| Sketch🕴️ | 0020265 | 2026-02-28 21:10:00 +0400 | [diff] [blame^] | 153 | if args.AssignTo != "" { |
| 154 | item.AssignedTo = args.AssignTo |
| 155 | } |
| Sketch🕴️ | 305f817 | 2026-02-27 13:58:43 +0400 | [diff] [blame] | 156 | return "done", nil |
| 157 | } |
| 158 | |
| 159 | func RegisterToDoTools(reg tools.Registry) { |
| 160 | reg.Add(tools.NewFuncTool("todo_item_add", ToDoAddItem, "Add new ToDo item.")) |
| 161 | reg.Add(tools.NewFuncTool("todo_item_mark_done", ToDoMarkItemDone, "Marks ToDo item with given ID as done.")) |
| 162 | reg.Add(tools.NewFuncTool("todo_item_add_comment", ToDoItemAddComment, "Adds discussion comment to given ToDo item")) |
| 163 | } |
| 164 | |
| 165 | func GetToolSchema(schema *jsonschema.Schema) (anthropic.ToolInputSchemaParam, error) { |
| 166 | schemaBytes, err := json.Marshal(schema) |
| 167 | if err != nil { |
| 168 | return anthropic.ToolInputSchemaParam{}, err |
| 169 | } |
| 170 | var schemaMap map[string]any |
| 171 | if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil { |
| 172 | return anthropic.ToolInputSchemaParam{}, err |
| 173 | } |
| 174 | |
| 175 | inputSchema, err := parseSchemaMap(schemaMap) |
| 176 | if err != nil { |
| 177 | return anthropic.ToolInputSchemaParam{}, err |
| 178 | } |
| 179 | return inputSchema, nil |
| 180 | } |
| 181 | |
| 182 | func parseSchemaMap(s map[string]any) (anthropic.ToolInputSchemaParam, error) { |
| 183 | bytes, err := json.Marshal(s) |
| 184 | if err != nil { |
| 185 | return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to marshal schema: %w", err) |
| 186 | } |
| 187 | |
| 188 | var schema anthropic.ToolInputSchemaParam |
| 189 | if err := json.Unmarshal(bytes, &schema); err != nil { |
| 190 | return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to unmarshal schema: %w", err) |
| 191 | } |
| 192 | |
| 193 | return schema, nil |
| 194 | } |