Initial commit
diff --git a/loop/agent_test.go b/loop/agent_test.go
new file mode 100644
index 0000000..b9f9994
--- /dev/null
+++ b/loop/agent_test.go
@@ -0,0 +1,154 @@
+package loop
+
+import (
+	"context"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+	"time"
+
+	"sketch.dev/ant"
+	"sketch.dev/httprr"
+)
+
+// TestAgentLoop tests that the Agent loop functionality works correctly.
+// It uses the httprr package to record HTTP interactions for replay in tests.
+// When failing, rebuild with "go test ./sketch/loop -run TestAgentLoop -httprecord .*agent_loop.*"
+// as necessary.
+func TestAgentLoop(t *testing.T) {
+	ctx := context.Background()
+
+	// Setup httprr recorder
+	rrPath := "testdata/agent_loop.httprr"
+	rr, err := httprr.Open(rrPath, http.DefaultTransport)
+	if err != nil && !os.IsNotExist(err) {
+		t.Fatal(err)
+	}
+
+	if rr.Recording() {
+		// Skip the test if API key is not available
+		if os.Getenv("ANTHROPIC_API_KEY") == "" {
+			t.Fatal("ANTHROPIC_API_KEY not set, required for HTTP recording")
+		}
+	}
+
+	// Create HTTP client
+	var client *http.Client
+	if rr != nil {
+		// Scrub API keys from requests for security
+		rr.ScrubReq(func(req *http.Request) error {
+			req.Header.Del("x-api-key")
+			req.Header.Del("anthropic-api-key")
+			return nil
+		})
+		client = rr.Client()
+	} else {
+		client = &http.Client{Transport: http.DefaultTransport}
+	}
+
+	// Create a new agent with the httprr client
+	origWD, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := os.Chdir("/"); err != nil {
+		t.Fatal(err)
+	}
+	budget := ant.Budget{MaxResponses: 100}
+	wd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cfg := AgentConfig{
+		Context:      ctx,
+		APIKey:       os.Getenv("ANTHROPIC_API_KEY"),
+		HTTPC:        client,
+		Budget:       budget,
+		GitUsername:  "Test Agent",
+		GitEmail:     "totallyhuman@sketch.dev",
+		SessionID:    "test-session-id",
+		ClientGOOS:   "linux",
+		ClientGOARCH: "amd64",
+	}
+	agent := NewAgent(cfg)
+	if err := os.Chdir(origWD); err != nil {
+		t.Fatal(err)
+	}
+	err = agent.Init(AgentInit{WorkingDir: wd, NoGit: true})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Setup a test message that will trigger a simple, predictable response
+	userMessage := "What tools are available to you? Please just list them briefly."
+
+	// Send the message to the agent
+	agent.UserMessage(ctx, userMessage)
+
+	// Process a single loop iteration to avoid long-running tests
+	agent.InnerLoop(ctx)
+
+	// Collect responses with a timeout
+	var responses []AgentMessage
+	timeout := time.After(10 * time.Second)
+	done := false
+
+	for !done {
+		select {
+		case <-timeout:
+			t.Log("Timeout reached while waiting for agent responses")
+			done = true
+		default:
+			select {
+			case msg := <-agent.outbox:
+				t.Logf("Received message: Type=%s, EndOfTurn=%v, Content=%q", msg.Type, msg.EndOfTurn, msg.Content)
+				responses = append(responses, msg)
+				if msg.EndOfTurn {
+					done = true
+				}
+			default:
+				// No more messages available right now
+				time.Sleep(100 * time.Millisecond)
+			}
+		}
+	}
+
+	// Verify we got at least one response
+	if len(responses) == 0 {
+		t.Fatal("No responses received from agent")
+	}
+
+	// Log the received responses for debugging
+	t.Logf("Received %d responses", len(responses))
+
+	// Find the final agent response (with EndOfTurn=true)
+	var finalResponse *AgentMessage
+	for i := range responses {
+		if responses[i].Type == AgentMessageType && responses[i].EndOfTurn {
+			finalResponse = &responses[i]
+			break
+		}
+	}
+
+	// Verify we got a final agent response
+	if finalResponse == nil {
+		t.Fatal("No final agent response received")
+	}
+
+	// Check that the response contains tools information
+	if !strings.Contains(strings.ToLower(finalResponse.Content), "tool") {
+		t.Error("Expected response to mention tools")
+	}
+
+	// Count how many tool use messages we received
+	toolUseCount := 0
+	for _, msg := range responses {
+		if msg.Type == ToolUseMessageType {
+			toolUseCount++
+		}
+	}
+
+	t.Logf("Agent used %d tools in its response", toolUseCount)
+}