init
diff --git a/loop/anthropic.go b/loop/anthropic.go
new file mode 100644
index 0000000..8ff5d6b
--- /dev/null
+++ b/loop/anthropic.go
@@ -0,0 +1,103 @@
+package loop
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+	"time"
+
+	"dodo.cloud/neo/tools"
+
+	"github.com/anthropics/anthropic-sdk-go"
+)
+
+type AnthropicAgent struct {
+	reg    tools.Registry
+	client anthropic.Client
+}
+
+func (a *AnthropicAgent) Run(todo *ToDo) error {
+	sp := fmt.Sprintf(systemPrompt, ToDoJSONSchema())
+	var toolParams []anthropic.ToolParam
+	for _, t := range a.reg.All() {
+		schema, err := GetToolSchema(t.InputSchema())
+		if err != nil {
+			return err
+		}
+		toolParams = append(toolParams, anthropic.ToolParam{
+			Name:        t.Name(),
+			Description: anthropic.String(t.Description()),
+			InputSchema: schema,
+		})
+	}
+	tools := make([]anthropic.ToolUnionParam, len(toolParams))
+	for i, toolParam := range toolParams {
+		tools[i] = anthropic.ToolUnionParam{OfTool: &toolParam}
+	}
+	k, err := json.MarshalIndent(todo, "", "\t")
+	if err != nil {
+		return err
+	}
+	for {
+		items := findActionableItems(todo, "assistant")
+		if len(items) == 0 {
+			time.Sleep(30 * time.Second)
+			continue
+		}
+		var itemIds []string
+		var messages []anthropic.MessageParam
+		for _, i := range items {
+			itemIds = append(itemIds, i.ID)
+		}
+		messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(string(k))))
+		messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(
+			fmt.Sprintf("Work on TODO item: %s", strings.Join(itemIds, ", ")))))
+		for {
+			resp, err := a.client.Messages.New(context.TODO(), anthropic.MessageNewParams{
+				MaxTokens: 10240,
+				System: []anthropic.TextBlockParam{
+					{Text: sp},
+				},
+				Messages: messages,
+				Model:    anthropic.ModelClaudeOpus4_6,
+				Tools:    tools,
+			})
+			if err != nil {
+				return err
+			}
+			fmt.Printf("--- STOP_REASON: %s\n", resp.StopReason)
+			messages = append(messages, resp.ToParam())
+
+			var toolResults []anthropic.ContentBlockParamUnion
+			for _, block := range resp.Content {
+				switch v := block.AsAny().(type) {
+				case anthropic.TextBlock:
+					fmt.Printf("AI: %s\n", v.Text)
+				case anthropic.ToolUseBlock:
+					t := a.reg.Get(v.Name)
+					if t == nil {
+						toolResults = append(toolResults, anthropic.NewToolResultBlock(v.ID, fmt.Sprintf("unknown tool %q", v.Name), true))
+						continue
+					}
+					args := v.JSON.Input.Raw()
+					fmt.Printf("CALLING TOOL: %s %s\n", v.Name, args)
+					out, err := t.Call(string(args))
+					if err != nil {
+						fmt.Printf("ERR: %s\n", err.Error())
+						toolResults = append(toolResults, anthropic.NewToolResultBlock(v.ID, err.Error(), true))
+					} else {
+						toolResults = append(toolResults, anthropic.NewToolResultBlock(v.ID, out, false))
+					}
+				}
+			}
+			if len(toolResults) == 0 {
+				break
+			}
+			messages = append(messages, anthropic.NewUserMessage(toolResults...))
+		}
+		for _, i := range items {
+			i.AssignedTo = "user"
+		}
+	}
+}