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
+}