init
diff --git a/loop/loop.go b/loop/loop.go
new file mode 100644
index 0000000..6cfa111
--- /dev/null
+++ b/loop/loop.go
@@ -0,0 +1,209 @@
+package loop
+
+import (
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "sync"
+
+ "dodo.cloud/neo/tools"
+
+ "github.com/anthropics/anthropic-sdk-go"
+ "github.com/invopop/jsonschema"
+)
+
+//go:embed CONVERSATION_RULES.md
+var systemPrompt string
+
+type Message struct {
+ Author string `json:"author"`
+ Contents string `json:"contents"`
+ Done bool `json:"done"`
+}
+
+type Conversation struct {
+ Messages []Message `json:"message"`
+}
+
+var todo *ToDo
+
+func Run(pr PromptReader, client *Client, tools tools.Registry) error {
+ RegisterToDoTools(tools)
+ fmt.Printf("YOU: ")
+ prompt, err := pr.Read()
+ if err != nil {
+ return err
+ }
+ todo = &ToDo{}
+ todo.ID = "1"
+ todo.Title = prompt
+ todo.AssignedTo = "assistant"
+ agents := []Agent{
+ &UserAgent{pr},
+ &AnthropicAgent{tools, client.c},
+ }
+ var wg sync.WaitGroup
+ for _, a := range agents {
+ wg.Add(1)
+ go func() {
+ if err := a.Run(todo); err != nil {
+ panic(err)
+ }
+ wg.Done()
+ }()
+ }
+ wg.Wait()
+ return nil
+}
+
+// func Loop(todo *ToDo, item *ToDo, pr PromptReader, client *Client, reg tools.Registry) error {
+// messages := []anthropic.MessageParam{
+// anthropic.NewUserMessage(anthropic.NewTextBlock(string(k))),
+// anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Work on TODO item: %s", item.ID))),
+// }
+// for {
+// fmt.Println(todo.String())
+// if item.AssignedTo == "user" {
+// fmt.Printf("YOU %s: ", item.ID)
+// prompt, err := pr.Read()
+// if err != nil {
+// return err
+// }
+// item.Discussion = append(item.Discussion, Comment{
+// Author: "user",
+// Comment: prompt,
+// })
+// item.AssignedTo = "assistant"
+// messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)))
+// } else {
+// if len(messages) == 0 {
+// messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Work on TODO item with id"))))
+// }
+// }
+
+// }
+// }
+
+func pickToDoItem(todo *ToDo) *ToDo {
+ if todo.Done {
+ return nil
+ }
+ for _, i := range todo.Items {
+ if ret := pickToDoItem(i); ret != nil {
+ return ret
+ }
+ }
+ return todo
+}
+
+func findItemByID(todo *ToDo, id string) *ToDo {
+ if todo.ID == id {
+ return todo
+ }
+ for _, i := range todo.Items {
+ if ret := findItemByID(i, id); ret != nil {
+ return ret
+ }
+ }
+ return nil
+}
+
+type ToDoItem struct {
+ ParentID string `json:"parentId"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ AssignedTo string `json:"assignedTo"`
+}
+
+type ToDoAddItemArgs struct {
+ Items []ToDoItem `json:"items"`
+}
+
+func ToDoAddItem(args ToDoAddItemArgs) (string, error) {
+ for _, td := range args.Items {
+ item := findItemByID(todo, td.ParentID)
+ if item == nil {
+ return "error", fmt.Errorf("TODO item with given id not found: %s", td.ParentID)
+ }
+ id := fmt.Sprintf("%s.%d", item.ID, len(item.Items)+1)
+ item.Items = append(item.Items, &ToDo{
+ ID: id,
+ Title: td.Title,
+ Description: td.Description,
+ AssignedTo: td.AssignedTo,
+ })
+ }
+ return "done", nil
+}
+
+type ToDoMarkItemDoneArgs struct {
+ ID string `json:"id"`
+}
+
+func ToDoMarkItemDone(args ToDoMarkItemDoneArgs) (string, error) {
+ item := findItemByID(todo, args.ID)
+ if item == nil {
+ return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
+ }
+ item.Done = true
+ return "done", nil
+}
+
+type ToDoItemAddCommentArgs struct {
+ ID string `json:"id"`
+ Comment string `json:"comment"`
+ AssignTo string `json:"assignTo"`
+}
+
+func ToDoItemAddComment(args ToDoItemAddCommentArgs) (string, error) {
+ item := findItemByID(todo, args.ID)
+ if item == nil {
+ return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
+ }
+ if len(item.Discussion) == 0 {
+ return "error", fmt.Errorf("You shall never initiate a discussion, if you want to clarify something create a TODO item for it.")
+ }
+ item.Discussion = append(item.Discussion, Comment{
+ Author: "assistant",
+ Comment: args.Comment,
+ })
+ item.AssignedTo = args.AssignTo
+ return "done", nil
+}
+
+func RegisterToDoTools(reg tools.Registry) {
+ reg.Add(tools.NewFuncTool("todo_item_add", ToDoAddItem, "Add new ToDo item."))
+ reg.Add(tools.NewFuncTool("todo_item_mark_done", ToDoMarkItemDone, "Marks ToDo item with given ID as done."))
+ reg.Add(tools.NewFuncTool("todo_item_add_comment", ToDoItemAddComment, "Adds discussion comment to given ToDo item"))
+}
+
+func GetToolSchema(schema *jsonschema.Schema) (anthropic.ToolInputSchemaParam, error) {
+ schemaBytes, err := json.Marshal(schema)
+ if err != nil {
+ return anthropic.ToolInputSchemaParam{}, err
+ }
+ var schemaMap map[string]any
+ if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil {
+ return anthropic.ToolInputSchemaParam{}, err
+ }
+
+ inputSchema, err := parseSchemaMap(schemaMap)
+ if err != nil {
+ return anthropic.ToolInputSchemaParam{}, err
+ }
+ return inputSchema, nil
+}
+
+func parseSchemaMap(s map[string]any) (anthropic.ToolInputSchemaParam, error) {
+ bytes, err := json.Marshal(s)
+ if err != nil {
+ return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to marshal schema: %w", err)
+ }
+
+ var schema anthropic.ToolInputSchemaParam
+ if err := json.Unmarshal(bytes, &schema); err != nil {
+ return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to unmarshal schema: %w", err)
+ }
+
+ return schema, nil
+}