ant: delete

Somehow I managed to miss this in 4f84ab729ddbf428b0e891940f08f70b4edee05c.
diff --git a/ant/ant.go b/ant/ant.go
deleted file mode 100644
index 0829286..0000000
--- a/ant/ant.go
+++ /dev/null
@@ -1,1033 +0,0 @@
-package ant
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"log/slog"
-	"maps"
-	"math/rand/v2"
-	"net/http"
-	"slices"
-	"strings"
-	"sync"
-	"testing"
-	"time"
-
-	"github.com/oklog/ulid/v2"
-	"github.com/richardlehane/crock32"
-	"sketch.dev/skribe"
-)
-
-const (
-	DefaultModel = Claude37Sonnet
-	// See https://docs.anthropic.com/en/docs/about-claude/models/all-models for
-	// current maximums. There's currently a flag to enable 128k output (output-128k-2025-02-19)
-	DefaultMaxTokens = 8192
-	DefaultURL       = "https://api.anthropic.com/v1/messages"
-)
-
-const (
-	Claude35Sonnet = "claude-3-5-sonnet-20241022"
-	Claude35Haiku  = "claude-3-5-haiku-20241022"
-	Claude37Sonnet = "claude-3-7-sonnet-20250219"
-)
-
-const (
-	MessageRoleUser      = "user"
-	MessageRoleAssistant = "assistant"
-
-	ContentTypeText             = "text"
-	ContentTypeThinking         = "thinking"
-	ContentTypeRedactedThinking = "redacted_thinking"
-	ContentTypeToolUse          = "tool_use"
-	ContentTypeToolResult       = "tool_result"
-
-	StopReasonStopSequence = "stop_sequence"
-	StopReasonMaxTokens    = "max_tokens"
-	StopReasonEndTurn      = "end_turn"
-	StopReasonToolUse      = "tool_use"
-)
-
-type Listener interface {
-	// TODO: Content is leaking an anthropic API; should we avoid it?
-	// TODO: Where should we include start/end time and usage?
-	OnToolCall(ctx context.Context, convo *Convo, toolCallID string, toolName string, toolInput json.RawMessage, content Content)
-	OnToolResult(ctx context.Context, convo *Convo, toolCallID string, toolName string, toolInput json.RawMessage, content Content, result *string, err error)
-	OnRequest(ctx context.Context, convo *Convo, requestID string, msg *Message)
-	OnResponse(ctx context.Context, convo *Convo, requestID string, msg *MessageResponse)
-}
-
-type NoopListener struct{}
-
-func (n *NoopListener) OnToolCall(ctx context.Context, convo *Convo, id string, toolName string, toolInput json.RawMessage, content Content) {
-}
-
-func (n *NoopListener) OnToolResult(ctx context.Context, convo *Convo, id string, toolName string, toolInput json.RawMessage, content Content, result *string, err error) {
-}
-
-func (n *NoopListener) OnResponse(ctx context.Context, convo *Convo, id string, msg *MessageResponse) {
-}
-func (n *NoopListener) OnRequest(ctx context.Context, convo *Convo, id string, msg *Message) {}
-
-type Content struct {
-	// TODO: image support?
-	// https://docs.anthropic.com/en/api/messages
-	ID   string `json:"id,omitempty"`
-	Type string `json:"type,omitempty"`
-	Text string `json:"text,omitempty"`
-
-	// for thinking
-	Thinking  string `json:"thinking,omitempty"`
-	Data      string `json:"data,omitempty"`      // for redacted_thinking
-	Signature string `json:"signature,omitempty"` // for thinking
-
-	// for tool_use
-	ToolName  string          `json:"name,omitempty"`
-	ToolInput json.RawMessage `json:"input,omitempty"`
-
-	// for tool_result
-	ToolUseID  string `json:"tool_use_id,omitempty"`
-	ToolError  bool   `json:"is_error,omitempty"`
-	ToolResult string `json:"content,omitempty"`
-
-	// timing information for tool_result; not sent to Claude
-	StartTime *time.Time `json:"-"`
-	EndTime   *time.Time `json:"-"`
-
-	CacheControl json.RawMessage `json:"cache_control,omitempty"`
-}
-
-func StringContent(s string) Content {
-	return Content{Type: ContentTypeText, Text: s}
-}
-
-// Message represents a message in the conversation.
-type Message struct {
-	Role    string    `json:"role"`
-	Content []Content `json:"content"`
-	ToolUse *ToolUse  `json:"tool_use,omitempty"` // use to control whether/which tool to use
-}
-
-// ToolUse represents a tool use in the message content.
-type ToolUse struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-}
-
-// Tool represents a tool available to Claude.
-type Tool struct {
-	Name string `json:"name"`
-	// Type is used by the text editor tool; see
-	// https://docs.anthropic.com/en/docs/build-with-claude/tool-use/text-editor-tool
-	Type        string          `json:"type,omitempty"`
-	Description string          `json:"description,omitempty"`
-	InputSchema json.RawMessage `json:"input_schema,omitempty"`
-
-	// The Run function is automatically called when the tool is used.
-	// Run functions may be called concurrently with each other and themselves.
-	// The input to Run function is the input to the tool, as provided by Claude, in compliance with the input schema.
-	// The outputs from Run will be sent back to Claude.
-	// If you do not want to respond to the tool call request from Claude, return ErrDoNotRespond.
-	// ctx contains extra (rarely used) tool call information; retrieve it with ToolCallInfoFromContext.
-	Run func(ctx context.Context, input json.RawMessage) (string, error) `json:"-"`
-}
-
-var ErrDoNotRespond = errors.New("do not respond")
-
-// Usage represents the billing and rate-limit usage.
-type Usage struct {
-	InputTokens              uint64  `json:"input_tokens"`
-	CacheCreationInputTokens uint64  `json:"cache_creation_input_tokens"`
-	CacheReadInputTokens     uint64  `json:"cache_read_input_tokens"`
-	OutputTokens             uint64  `json:"output_tokens"`
-	CostUSD                  float64 `json:"cost_usd"`
-}
-
-func (u *Usage) Add(other Usage) {
-	u.InputTokens += other.InputTokens
-	u.CacheCreationInputTokens += other.CacheCreationInputTokens
-	u.CacheReadInputTokens += other.CacheReadInputTokens
-	u.OutputTokens += other.OutputTokens
-	u.CostUSD += other.CostUSD
-}
-
-func (u *Usage) String() string {
-	return fmt.Sprintf("in: %d, out: %d", u.InputTokens, u.OutputTokens)
-}
-
-func (u *Usage) IsZero() bool {
-	return *u == Usage{}
-}
-
-func (u *Usage) Attr() slog.Attr {
-	return slog.Group("usage",
-		slog.Uint64("input_tokens", u.InputTokens),
-		slog.Uint64("output_tokens", u.OutputTokens),
-		slog.Uint64("cache_creation_input_tokens", u.CacheCreationInputTokens),
-		slog.Uint64("cache_read_input_tokens", u.CacheReadInputTokens),
-	)
-}
-
-type ErrorResponse struct {
-	Type    string `json:"type"`
-	Message string `json:"message"`
-}
-
-// MessageResponse represents the response from the message API.
-type MessageResponse struct {
-	ID           string     `json:"id"`
-	Type         string     `json:"type"`
-	Role         string     `json:"role"`
-	Model        string     `json:"model"`
-	Content      []Content  `json:"content"`
-	StopReason   string     `json:"stop_reason"`
-	StopSequence *string    `json:"stop_sequence,omitempty"`
-	Usage        Usage      `json:"usage"`
-	StartTime    *time.Time `json:"start_time,omitempty"`
-	EndTime      *time.Time `json:"end_time,omitempty"`
-}
-
-func (m *MessageResponse) ToMessage() Message {
-	return Message{
-		Role:    m.Role,
-		Content: m.Content,
-	}
-}
-
-func (m *MessageResponse) StopSequenceString() string {
-	if m.StopSequence == nil {
-		return ""
-	}
-	return *m.StopSequence
-}
-
-const (
-	ToolChoiceTypeAuto = "auto" // default
-	ToolChoiceTypeAny  = "any"  // any tool, but must use one
-	ToolChoiceTypeNone = "none" // no tools allowed
-	ToolChoiceTypeTool = "tool" // must use the tool specified in the Name field
-)
-
-type ToolChoice struct {
-	Type string `json:"type"`
-	Name string `json:"name,omitempty"`
-}
-
-// https://docs.anthropic.com/en/api/messages#body-system
-type SystemContent struct {
-	Text         string          `json:"text,omitempty"`
-	Type         string          `json:"type,omitempty"`
-	CacheControl json.RawMessage `json:"cache_control,omitempty"`
-}
-
-// MessageRequest represents the request payload for creating a message.
-type MessageRequest struct {
-	Model         string          `json:"model"`
-	Messages      []Message       `json:"messages"`
-	ToolChoice    *ToolChoice     `json:"tool_choice,omitempty"`
-	MaxTokens     int             `json:"max_tokens"`
-	Tools         []*Tool         `json:"tools,omitempty"`
-	Stream        bool            `json:"stream,omitempty"`
-	System        []SystemContent `json:"system,omitempty"`
-	Temperature   float64         `json:"temperature,omitempty"`
-	TopK          int             `json:"top_k,omitempty"`
-	TopP          float64         `json:"top_p,omitempty"`
-	StopSequences []string        `json:"stop_sequences,omitempty"`
-
-	TokenEfficientToolUse bool `json:"-"` // DO NOT USE, broken on Anthropic's side as of 2025-02-28
-}
-
-const dumpText = false // debugging toggle to see raw communications with Claude
-
-// createMessage sends a request to the Anthropic message API to create a message.
-func createMessage(ctx context.Context, httpc *http.Client, url, apiKey string, request *MessageRequest) (*MessageResponse, error) {
-	var payload []byte
-	var err error
-	if dumpText || testing.Testing() {
-		payload, err = json.MarshalIndent(request, "", " ")
-	} else {
-		payload, err = json.Marshal(request)
-		payload = append(payload, '\n')
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	if false {
-		fmt.Printf("claude request payload:\n%s\n", payload)
-	}
-
-	backoff := []time.Duration{15 * time.Second, 30 * time.Second, time.Minute}
-	largerMaxTokens := false
-	var partialUsage Usage
-
-	// retry loop
-	for attempts := 0; ; attempts++ {
-		if dumpText {
-			fmt.Printf("RAW REQUEST:\n%s\n\n", payload)
-		}
-		req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload))
-		if err != nil {
-			return nil, err
-		}
-
-		req.Header.Set("Content-Type", "application/json")
-		req.Header.Set("X-API-Key", apiKey)
-		req.Header.Set("Anthropic-Version", "2023-06-01")
-
-		features := []string{}
-
-		if request.TokenEfficientToolUse {
-			features = append(features, "token-efficient-tool-use-2025-02-19")
-		}
-		if largerMaxTokens {
-			features = append(features, "output-128k-2025-02-19")
-			request.MaxTokens = 128 * 1024
-		}
-		if len(features) > 0 {
-			req.Header.Set("anthropic-beta", strings.Join(features, ","))
-		}
-
-		resp, err := httpc.Do(req)
-		if err != nil {
-			return nil, err
-		}
-		buf, _ := io.ReadAll(resp.Body)
-		resp.Body.Close()
-
-		switch {
-		case resp.StatusCode == http.StatusOK:
-			if dumpText {
-				fmt.Printf("RAW RESPONSE:\n%s\n\n", buf)
-			}
-			var response MessageResponse
-			err = json.NewDecoder(bytes.NewReader(buf)).Decode(&response)
-			if err != nil {
-				return nil, err
-			}
-			if response.StopReason == StopReasonMaxTokens && !largerMaxTokens {
-				fmt.Printf("Retrying Anthropic API call with larger max tokens size.")
-				// Retry with more output tokens.
-				largerMaxTokens = true
-				response.Usage.CostUSD = response.TotalDollars()
-				partialUsage = response.Usage
-				continue
-			}
-
-			// Calculate and set the cost_usd field
-			if largerMaxTokens {
-				response.Usage.Add(partialUsage)
-			}
-			response.Usage.CostUSD = response.TotalDollars()
-
-			return &response, nil
-		case resp.StatusCode >= 500 && resp.StatusCode < 600:
-			// overloaded or unhappy, in one form or another
-			sleep := backoff[min(attempts, len(backoff)-1)] + time.Duration(rand.Int64N(int64(time.Second)))
-			slog.WarnContext(ctx, "anthropic_request_failed", "response", string(buf), "status_code", resp.StatusCode, "sleep", sleep)
-			time.Sleep(sleep)
-		case resp.StatusCode == 429:
-			// rate limited. wait 1 minute as a starting point, because that's the rate limiting window.
-			// and then add some additional time for backoff.
-			sleep := time.Minute + backoff[min(attempts, len(backoff)-1)] + time.Duration(rand.Int64N(int64(time.Second)))
-			slog.WarnContext(ctx, "anthropic_request_rate_limited", "response", string(buf), "sleep", sleep)
-			time.Sleep(sleep)
-		// case resp.StatusCode == 400:
-		// TODO: parse ErrorResponse, make (*ErrorResponse) implement error
-		default:
-			return nil, fmt.Errorf("API request failed with status %s\n%s", resp.Status, buf)
-		}
-	}
-}
-
-// A Convo is a managed conversation with Claude.
-// It automatically manages the state of the conversation,
-// including appending messages send/received,
-// calling tools and sending their results,
-// tracking usage, etc.
-//
-// Exported fields must not be altered concurrently with calling any method on Convo.
-// Typical usage is to configure a Convo once before using it.
-type Convo struct {
-	// ID is a unique ID for the conversation
-	ID string
-	// Ctx is the context for the entire conversation.
-	Ctx context.Context
-	// HTTPC is the HTTP client for the conversation.
-	HTTPC *http.Client
-	// URL is the remote messages URL to dial.
-	URL string
-	// APIKey is the API key for the conversation.
-	APIKey string
-	// Model is the model for the conversation.
-	Model string
-	// MaxTokens is the max tokens for each response in the conversation.
-	MaxTokens int
-	// Tools are the tools available during the conversation.
-	Tools []*Tool
-	// SystemPrompt is the system prompt for the conversation.
-	SystemPrompt string
-	// PromptCaching indicates whether to use Anthropic's prompt caching.
-	// See https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#continuing-a-multi-turn-conversation
-	// for the documentation. At request send time, we set the cache_control field on the
-	// last message. We also cache the system prompt.
-	// Default: true.
-	PromptCaching bool
-	// ToolUseOnly indicates whether Claude may only use tools during this conversation.
-	// TODO: add more fine-grained control over tool use?
-	ToolUseOnly bool
-	// Parent is the parent conversation, if any.
-	// It is non-nil for "subagent" calls.
-	// It is set automatically when calling SubConvo,
-	// and usually should not be set manually.
-	Parent *Convo
-	// Budget is the budget for this conversation (and all sub-conversations).
-	// The Conversation DOES NOT automatically enforce the budget.
-	// It is up to the caller to call OverBudget() as appropriate.
-	Budget Budget
-
-	// messages tracks the messages so far in the conversation.
-	messages []Message
-
-	// Listener receives messages being sent.
-	Listener Listener
-
-	muToolUseCancel *sync.Mutex
-	toolUseCancel   map[string]context.CancelCauseFunc
-
-	// Protects usage. This is used for subconversations (that share part of CumulativeUsage) as well.
-	mu *sync.Mutex
-	// usage tracks usage for this conversation and all sub-conversations.
-	usage *CumulativeUsage
-}
-
-// newConvoID generates a new 8-byte random id.
-// The uniqueness/collision requirements here are very low.
-// They are not global identifiers,
-// just enough to distinguish different convos in a single session.
-func newConvoID() string {
-	u1 := rand.Uint32()
-	s := crock32.Encode(uint64(u1))
-	if len(s) < 7 {
-		s += strings.Repeat("0", 7-len(s))
-	}
-	return s[:3] + "-" + s[3:]
-}
-
-// NewConvo creates a new conversation with Claude with sensible defaults.
-// ctx is the context for the entire conversation.
-func NewConvo(ctx context.Context, apiKey string) *Convo {
-	id := newConvoID()
-	return &Convo{
-		Ctx:             skribe.ContextWithAttr(ctx, slog.String("convo_id", id)),
-		HTTPC:           http.DefaultClient,
-		URL:             DefaultURL,
-		APIKey:          apiKey,
-		Model:           DefaultModel,
-		MaxTokens:       DefaultMaxTokens,
-		PromptCaching:   true,
-		usage:           newUsage(),
-		Listener:        &NoopListener{},
-		ID:              id,
-		muToolUseCancel: &sync.Mutex{},
-		toolUseCancel:   map[string]context.CancelCauseFunc{},
-		mu:              &sync.Mutex{},
-	}
-}
-
-// SubConvo creates a sub-conversation with the same configuration as the parent conversation.
-// (This propagates context for cancellation, HTTP client, API key, etc.)
-// The sub-conversation shares no messages with the parent conversation.
-// It does not inherit tools from the parent conversation.
-func (c *Convo) SubConvo() *Convo {
-	id := newConvoID()
-	return &Convo{
-		Ctx:           skribe.ContextWithAttr(c.Ctx, slog.String("convo_id", id), slog.String("parent_convo_id", c.ID)),
-		HTTPC:         c.HTTPC,
-		URL:           c.URL,
-		APIKey:        c.APIKey,
-		Model:         c.Model,
-		MaxTokens:     c.MaxTokens,
-		PromptCaching: c.PromptCaching,
-		Parent:        c,
-		// For convenience, sub-convo usage shares tool uses map with parent,
-		// all other fields separate, propagated in AddResponse
-		usage:    newUsageWithSharedToolUses(c.usage),
-		mu:       c.mu,
-		Listener: c.Listener,
-		ID:       id,
-		// Do not copy Budget. Each budget is independent,
-		// and OverBudget checks whether any ancestor is over budget.
-	}
-}
-
-func (c *Convo) SubConvoWithHistory() *Convo {
-	id := newConvoID()
-	return &Convo{
-		Ctx:           skribe.ContextWithAttr(c.Ctx, slog.String("convo_id", id), slog.String("parent_convo_id", c.ID)),
-		HTTPC:         c.HTTPC,
-		URL:           c.URL,
-		APIKey:        c.APIKey,
-		Model:         c.Model,
-		MaxTokens:     c.MaxTokens,
-		PromptCaching: c.PromptCaching,
-		Parent:        c,
-		// For convenience, sub-convo usage shares tool uses map with parent,
-		// all other fields separate, propagated in AddResponse
-		usage:    newUsageWithSharedToolUses(c.usage),
-		mu:       c.mu,
-		Listener: c.Listener,
-		ID:       id,
-		// Do not copy Budget. Each budget is independent,
-		// and OverBudget checks whether any ancestor is over budget.
-		messages: slices.Clone(c.messages),
-	}
-}
-
-// Depth reports how many "sub-conversations" deep this conversation is.
-// That it, it walks up parents until it finds a root.
-func (c *Convo) Depth() int {
-	x := c
-	var depth int
-	for x.Parent != nil {
-		x = x.Parent
-		depth++
-	}
-	return depth
-}
-
-// SendUserTextMessage sends a text message to Claude in this conversation.
-// otherContents contains additional contents to send with the message, usually tool results.
-func (c *Convo) SendUserTextMessage(s string, otherContents ...Content) (*MessageResponse, error) {
-	contents := slices.Clone(otherContents)
-	if s != "" {
-		contents = append(contents, StringContent(s))
-	}
-	msg := Message{
-		Role:    MessageRoleUser,
-		Content: contents,
-	}
-	return c.SendMessage(msg)
-}
-
-func (c *Convo) messageRequest(msg Message) *MessageRequest {
-	system := []SystemContent{}
-	if c.SystemPrompt != "" {
-		var d SystemContent
-		d = SystemContent{Type: ContentTypeText, Text: c.SystemPrompt}
-		if c.PromptCaching {
-			d.CacheControl = json.RawMessage(`{"type":"ephemeral"}`)
-		}
-		system = []SystemContent{d}
-	}
-
-	// Claude is happy to return an empty response in response to our Done() call,
-	// and, if so, you'll see something like:
-	// API request failed with status 400 Bad Request
-	// {"type":"error","error":  {"type":"invalid_request_error",
-	// "message":"messages.5: all messages must have non-empty content except for the optional final assistant message"}}
-	// So, we filter out those empty messages.
-	var nonEmptyMessages []Message
-	for _, m := range c.messages {
-		if len(m.Content) > 0 {
-			nonEmptyMessages = append(nonEmptyMessages, m)
-		}
-	}
-
-	mr := &MessageRequest{
-		Model:     c.Model,
-		Messages:  append(nonEmptyMessages, msg), // not yet committed to keeping msg
-		System:    system,
-		Tools:     c.Tools,
-		MaxTokens: c.MaxTokens,
-	}
-	if c.ToolUseOnly {
-		mr.ToolChoice = &ToolChoice{Type: ToolChoiceTypeAny}
-	}
-	return mr
-}
-
-func (c *Convo) findTool(name string) (*Tool, error) {
-	for _, tool := range c.Tools {
-		if tool.Name == name {
-			return tool, nil
-		}
-	}
-	return nil, fmt.Errorf("tool %q not found", name)
-}
-
-// insertMissingToolResults adds error results for tool uses that were requested
-// but not included in the message, which can happen in error paths like "out of budget."
-// We only insert these if there were no tool responses at all, since an incorrect
-// number of tool results would be a programmer error. Mutates inputs.
-func (c *Convo) insertMissingToolResults(mr *MessageRequest, msg *Message) {
-	if len(mr.Messages) < 2 {
-		return
-	}
-	prev := mr.Messages[len(mr.Messages)-2]
-	var toolUsePrev int
-	for _, c := range prev.Content {
-		if c.Type == ContentTypeToolUse {
-			toolUsePrev++
-		}
-	}
-	if toolUsePrev == 0 {
-		return
-	}
-	var toolUseCurrent int
-	for _, c := range msg.Content {
-		if c.Type == ContentTypeToolResult {
-			toolUseCurrent++
-		}
-	}
-	if toolUseCurrent != 0 {
-		return
-	}
-	var prefix []Content
-	for _, part := range prev.Content {
-		if part.Type != ContentTypeToolUse {
-			continue
-		}
-		content := Content{
-			Type:       ContentTypeToolResult,
-			ToolUseID:  part.ID,
-			ToolError:  true,
-			ToolResult: "not executed; retry possible",
-		}
-		prefix = append(prefix, content)
-		msg.Content = append(prefix, msg.Content...)
-		mr.Messages[len(mr.Messages)-1].Content = msg.Content
-	}
-	slog.DebugContext(c.Ctx, "inserted missing tool results")
-}
-
-// SendMessage sends a message to Claude.
-// The conversation records (internally) all messages succesfully sent and received.
-func (c *Convo) SendMessage(msg Message) (*MessageResponse, error) {
-	id := ulid.Make().String()
-	mr := c.messageRequest(msg)
-	var lastMessage *Message
-	if c.PromptCaching {
-		lastMessage = &mr.Messages[len(mr.Messages)-1]
-		if len(lastMessage.Content) > 0 {
-			lastMessage.Content[len(lastMessage.Content)-1].CacheControl = json.RawMessage(`{"type":"ephemeral"}`)
-		}
-	}
-	defer func() {
-		if lastMessage == nil {
-			return
-		}
-		if len(lastMessage.Content) > 0 {
-			lastMessage.Content[len(lastMessage.Content)-1].CacheControl = []byte{}
-		}
-	}()
-	c.insertMissingToolResults(mr, &msg)
-	c.Listener.OnRequest(c.Ctx, c, id, &msg)
-
-	startTime := time.Now()
-	resp, err := createMessage(c.Ctx, c.HTTPC, c.URL, c.APIKey, mr)
-	if resp != nil {
-		resp.StartTime = &startTime
-		endTime := time.Now()
-		resp.EndTime = &endTime
-	}
-
-	if err != nil {
-		c.Listener.OnResponse(c.Ctx, c, id, nil)
-		return nil, err
-	}
-	c.messages = append(c.messages, msg, resp.ToMessage())
-	// Propagate usage to all ancestors (including us).
-	for x := c; x != nil; x = x.Parent {
-		x.usage.AddResponse(resp)
-	}
-	c.Listener.OnResponse(c.Ctx, c, id, resp)
-	return resp, err
-}
-
-type toolCallInfoKeyType string
-
-var toolCallInfoKey toolCallInfoKeyType
-
-type ToolCallInfo struct {
-	ToolUseID string
-	Convo     *Convo
-}
-
-func ToolCallInfoFromContext(ctx context.Context) ToolCallInfo {
-	v := ctx.Value(toolCallInfoKey)
-	i, _ := v.(ToolCallInfo)
-	return i
-}
-
-func (c *Convo) ToolResultCancelContents(resp *MessageResponse) ([]Content, error) {
-	if resp.StopReason != StopReasonToolUse {
-		return nil, nil
-	}
-	var toolResults []Content
-
-	for _, part := range resp.Content {
-		if part.Type != ContentTypeToolUse {
-			continue
-		}
-		c.incrementToolUse(part.ToolName)
-
-		content := Content{
-			Type:      ContentTypeToolResult,
-			ToolUseID: part.ID,
-		}
-
-		content.ToolError = true
-		content.ToolResult = "user canceled this too_use"
-		toolResults = append(toolResults, content)
-	}
-	return toolResults, nil
-}
-
-// GetID returns the conversation ID
-func (c *Convo) GetID() string {
-	return c.ID
-}
-
-func (c *Convo) CancelToolUse(toolUseID string, err error) error {
-	c.muToolUseCancel.Lock()
-	defer c.muToolUseCancel.Unlock()
-	cancel, ok := c.toolUseCancel[toolUseID]
-	if !ok {
-		return fmt.Errorf("cannot cancel %s: no cancel function registered for this tool_use_id. All I have is %+v", toolUseID, c.toolUseCancel)
-	}
-	delete(c.toolUseCancel, toolUseID)
-	cancel(err)
-	return nil
-}
-
-func (c *Convo) newToolUseContext(ctx context.Context, toolUseID string) (context.Context, context.CancelFunc) {
-	c.muToolUseCancel.Lock()
-	defer c.muToolUseCancel.Unlock()
-	ctx, cancel := context.WithCancelCause(ctx)
-	c.toolUseCancel[toolUseID] = cancel
-	return ctx, func() { c.CancelToolUse(toolUseID, nil) }
-}
-
-// ToolResultContents runs all tool uses requested by the response and returns their results.
-// Cancelling ctx will cancel any running tool calls.
-func (c *Convo) ToolResultContents(ctx context.Context, resp *MessageResponse) ([]Content, error) {
-	if resp.StopReason != StopReasonToolUse {
-		return nil, nil
-	}
-	// Extract all tool calls from the response, call the tools, and gather the results.
-	var wg sync.WaitGroup
-	toolResultC := make(chan Content, len(resp.Content))
-	for _, part := range resp.Content {
-		if part.Type != ContentTypeToolUse {
-			continue
-		}
-		c.incrementToolUse(part.ToolName)
-		startTime := time.Now()
-
-		c.Listener.OnToolCall(ctx, c, part.ID, part.ToolName, part.ToolInput, Content{
-			Type:      ContentTypeToolUse,
-			ToolUseID: part.ID,
-			StartTime: &startTime,
-		})
-
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
-
-			content := Content{
-				Type:      ContentTypeToolResult,
-				ToolUseID: part.ID,
-				StartTime: &startTime,
-			}
-			sendErr := func(err error) {
-				// Record end time
-				endTime := time.Now()
-				content.EndTime = &endTime
-
-				content.ToolError = true
-				content.ToolResult = err.Error()
-				c.Listener.OnToolResult(ctx, c, part.ID, part.ToolName, part.ToolInput, content, nil, err)
-				toolResultC <- content
-			}
-			sendRes := func(res string) {
-				// Record end time
-				endTime := time.Now()
-				content.EndTime = &endTime
-
-				content.ToolResult = res
-				c.Listener.OnToolResult(ctx, c, part.ID, part.ToolName, part.ToolInput, content, &res, nil)
-				toolResultC <- content
-			}
-
-			tool, err := c.findTool(part.ToolName)
-			if err != nil {
-				sendErr(err)
-				return
-			}
-			// Create a new context for just this tool_use call, and register its
-			// cancel function so that it can be canceled individually.
-			toolUseCtx, cancel := c.newToolUseContext(ctx, part.ID)
-			defer cancel()
-			// TODO: move this into newToolUseContext?
-			toolUseCtx = context.WithValue(toolUseCtx, toolCallInfoKey, ToolCallInfo{ToolUseID: part.ID, Convo: c})
-			toolResult, err := tool.Run(toolUseCtx, part.ToolInput)
-			if errors.Is(err, ErrDoNotRespond) {
-				return
-			}
-			if toolUseCtx.Err() != nil {
-				sendErr(context.Cause(toolUseCtx))
-				return
-			}
-
-			if err != nil {
-				sendErr(err)
-				return
-			}
-			sendRes(toolResult)
-		}()
-	}
-	wg.Wait()
-	close(toolResultC)
-	var toolResults []Content
-	for toolResult := range toolResultC {
-		toolResults = append(toolResults, toolResult)
-	}
-	if ctx.Err() != nil {
-		return nil, ctx.Err()
-	}
-	return toolResults, nil
-}
-
-func (c *Convo) incrementToolUse(name string) {
-	c.mu.Lock()
-	defer c.mu.Unlock()
-
-	c.usage.ToolUses[name]++
-}
-
-// ContentsAttr returns contents as a slog.Attr.
-// It is meant for logging.
-func ContentsAttr(contents []Content) slog.Attr {
-	var contentAttrs []any // slog.Attr
-	for _, content := range contents {
-		var attrs []any // slog.Attr
-		switch content.Type {
-		case ContentTypeText:
-			attrs = append(attrs, slog.String("text", content.Text))
-		case ContentTypeToolUse:
-			attrs = append(attrs, slog.String("tool_name", content.ToolName))
-			attrs = append(attrs, slog.String("tool_input", string(content.ToolInput)))
-		case ContentTypeToolResult:
-			attrs = append(attrs, slog.String("tool_result", content.ToolResult))
-			attrs = append(attrs, slog.Bool("tool_error", content.ToolError))
-		case ContentTypeThinking:
-			attrs = append(attrs, slog.String("thinking", content.Text))
-		default:
-			attrs = append(attrs, slog.String("unknown_content_type", content.Type))
-			attrs = append(attrs, slog.Any("text", content)) // just log it all raw, better to have too much than not enough
-		}
-		contentAttrs = append(contentAttrs, slog.Group(content.ID, attrs...))
-	}
-	return slog.Group("contents", contentAttrs...)
-}
-
-// MustSchema validates that schema is a valid JSON schema and returns it as a json.RawMessage.
-// It panics if the schema is invalid.
-func MustSchema(schema string) json.RawMessage {
-	// TODO: validate schema, for now just make sure it's valid JSON
-	schema = strings.TrimSpace(schema)
-	bytes := []byte(schema)
-	if !json.Valid(bytes) {
-		panic("invalid JSON schema: " + schema)
-	}
-	return json.RawMessage(bytes)
-}
-
-// cents per million tokens
-// (not dollars because i'm twitchy about using floats for money)
-type centsPer1MTokens struct {
-	Input         uint64
-	Output        uint64
-	CacheRead     uint64
-	CacheCreation uint64
-}
-
-// https://www.anthropic.com/pricing#anthropic-api
-var modelCost = map[string]centsPer1MTokens{
-	Claude37Sonnet: {
-		Input:         300,  // $3
-		Output:        1500, // $15
-		CacheRead:     30,   // $0.30
-		CacheCreation: 375,  // $3.75
-	},
-	Claude35Haiku: {
-		Input:         80,  // $0.80
-		Output:        400, // $4.00
-		CacheRead:     8,   // $0.08
-		CacheCreation: 100, // $1.00
-	},
-	Claude35Sonnet: {
-		Input:         300,  // $3
-		Output:        1500, // $15
-		CacheRead:     30,   // $0.30
-		CacheCreation: 375,  // $3.75
-	},
-}
-
-// TotalDollars returns the total cost to obtain this response, in dollars.
-func (mr *MessageResponse) TotalDollars() float64 {
-	cpm, ok := modelCost[mr.Model]
-	if !ok {
-		panic(fmt.Sprintf("no pricing info for model: %s", mr.Model))
-	}
-	use := mr.Usage
-	megaCents := use.InputTokens*cpm.Input +
-		use.OutputTokens*cpm.Output +
-		use.CacheReadInputTokens*cpm.CacheRead +
-		use.CacheCreationInputTokens*cpm.CacheCreation
-	cents := float64(megaCents) / 1_000_000.0
-	return cents / 100.0
-}
-
-func newUsage() *CumulativeUsage {
-	return &CumulativeUsage{ToolUses: make(map[string]int), StartTime: time.Now()}
-}
-
-func newUsageWithSharedToolUses(parent *CumulativeUsage) *CumulativeUsage {
-	return &CumulativeUsage{ToolUses: parent.ToolUses, StartTime: time.Now()}
-}
-
-// CumulativeUsage represents cumulative usage across a Convo, including all sub-conversations.
-type CumulativeUsage struct {
-	StartTime                time.Time      `json:"start_time"`
-	Responses                uint64         `json:"messages"` // count of responses
-	InputTokens              uint64         `json:"input_tokens"`
-	OutputTokens             uint64         `json:"output_tokens"`
-	CacheReadInputTokens     uint64         `json:"cache_read_input_tokens"`
-	CacheCreationInputTokens uint64         `json:"cache_creation_input_tokens"`
-	TotalCostUSD             float64        `json:"total_cost_usd"`
-	ToolUses                 map[string]int `json:"tool_uses"` // tool name -> number of uses
-}
-
-func (u *CumulativeUsage) Clone() CumulativeUsage {
-	v := *u
-	v.ToolUses = maps.Clone(u.ToolUses)
-	return v
-}
-
-func (c *Convo) CumulativeUsage() CumulativeUsage {
-	if c == nil {
-		return CumulativeUsage{}
-	}
-	c.mu.Lock()
-	defer c.mu.Unlock()
-	return c.usage.Clone()
-}
-
-func (u *CumulativeUsage) WallTime() time.Duration {
-	return time.Since(u.StartTime)
-}
-
-func (u *CumulativeUsage) DollarsPerHour() float64 {
-	hours := u.WallTime().Hours()
-	if hours == 0 {
-		return 0
-	}
-	return u.TotalCostUSD / hours
-}
-
-func (u *CumulativeUsage) AddResponse(resp *MessageResponse) {
-	usage := resp.Usage
-	u.Responses++
-	u.InputTokens += usage.InputTokens
-	u.OutputTokens += usage.OutputTokens
-	u.CacheReadInputTokens += usage.CacheReadInputTokens
-	u.CacheCreationInputTokens += usage.CacheCreationInputTokens
-	u.TotalCostUSD += resp.TotalDollars()
-}
-
-// TotalInputTokens returns the grand total cumulative input tokens in u.
-func (u *CumulativeUsage) TotalInputTokens() uint64 {
-	return u.InputTokens + u.CacheReadInputTokens + u.CacheCreationInputTokens
-}
-
-// Attr returns the cumulative usage as a slog.Attr with key "usage".
-func (u CumulativeUsage) Attr() slog.Attr {
-	elapsed := time.Since(u.StartTime)
-	return slog.Group("usage",
-		slog.Duration("wall_time", elapsed),
-		slog.Uint64("responses", u.Responses),
-		slog.Uint64("input_tokens", u.InputTokens),
-		slog.Uint64("output_tokens", u.OutputTokens),
-		slog.Uint64("cache_read_input_tokens", u.CacheReadInputTokens),
-		slog.Uint64("cache_creation_input_tokens", u.CacheCreationInputTokens),
-		slog.Float64("total_cost_usd", u.TotalCostUSD),
-		slog.Float64("dollars_per_hour", u.TotalCostUSD/elapsed.Hours()),
-		slog.Any("tool_uses", maps.Clone(u.ToolUses)),
-	)
-}
-
-// A Budget represents the maximum amount of resources that may be spent on a conversation.
-// Note that the default (zero) budget is unlimited.
-type Budget struct {
-	MaxResponses uint64        // if > 0, max number of iterations (=responses)
-	MaxDollars   float64       // if > 0, max dollars that may be spent
-	MaxWallTime  time.Duration // if > 0, max wall time that may be spent
-}
-
-// OverBudget returns an error if the convo (or any of its parents) has exceeded its budget.
-// TODO: document parent vs sub budgets, multiple errors, etc, once we know the desired behavior.
-func (c *Convo) OverBudget() error {
-	for x := c; x != nil; x = x.Parent {
-		if err := x.overBudget(); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// ResetBudget sets the budget to the passed in budget and
-// adjusts it by what's been used so far.
-func (c *Convo) ResetBudget(budget Budget) {
-	c.Budget = budget
-	if c.Budget.MaxDollars > 0 {
-		c.Budget.MaxDollars += c.CumulativeUsage().TotalCostUSD
-	}
-	if c.Budget.MaxResponses > 0 {
-		c.Budget.MaxResponses += c.CumulativeUsage().Responses
-	}
-	if c.Budget.MaxWallTime > 0 {
-		c.Budget.MaxWallTime += c.usage.WallTime()
-	}
-}
-
-func (c *Convo) overBudget() error {
-	usage := c.CumulativeUsage()
-	// TODO: stop before we exceed the budget instead of after?
-	// Top priority is money, then time, then response count.
-	var err error
-	cont := "Continuing to chat will reset the budget."
-	if c.Budget.MaxDollars > 0 && usage.TotalCostUSD >= c.Budget.MaxDollars {
-		err = errors.Join(err, fmt.Errorf("$%.2f spent, budget is $%.2f. %s", usage.TotalCostUSD, c.Budget.MaxDollars, cont))
-	}
-	if c.Budget.MaxWallTime > 0 && usage.WallTime() >= c.Budget.MaxWallTime {
-		err = errors.Join(err, fmt.Errorf("%v elapsed, budget is %v. %s", usage.WallTime().Truncate(time.Second), c.Budget.MaxWallTime.Truncate(time.Second), cont))
-	}
-	if c.Budget.MaxResponses > 0 && usage.Responses >= c.Budget.MaxResponses {
-		err = errors.Join(err, fmt.Errorf("%d responses received, budget is %d. %s", usage.Responses, c.Budget.MaxResponses, cont))
-	}
-	return err
-}
-
-// UserStringMessage creates a user message with a single text content item.
-func UserStringMessage(text string) Message {
-	return Message{
-		Role:    MessageRoleUser,
-		Content: []Content{StringContent(text)},
-	}
-}