all: support openai-compatible models
The support is rather minimal at this point:
Only hard-coded models, only -unsafe, only -skabandaddr="".
The "shared" LLM package is strongly Claude-flavored.
We can fix all of this and more over time, if we are inspired to.
(Maybe we'll switch to https://github.com/maruel/genai?)
The goal for now is to get the rough structure in place.
I've rebased and rebuilt this more times than I care to remember.
diff --git a/loop/agent.go b/loop/agent.go
index 3076385..960bf5a 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -17,10 +17,11 @@
"sync"
"time"
- "sketch.dev/ant"
"sketch.dev/browser"
"sketch.dev/claudetool"
"sketch.dev/claudetool/bashkit"
+ "sketch.dev/llm"
+ "sketch.dev/llm/conversation"
)
const (
@@ -64,8 +65,8 @@
// Returns the current number of messages in the history
MessageCount() int
- TotalUsage() ant.CumulativeUsage
- OriginalBudget() ant.Budget
+ TotalUsage() conversation.CumulativeUsage
+ OriginalBudget() conversation.Budget
WorkingDir() string
@@ -150,7 +151,7 @@
Timestamp time.Time `json:"timestamp"`
ConversationID string `json:"conversation_id"`
ParentConversationID *string `json:"parent_conversation_id,omitempty"`
- Usage *ant.Usage `json:"usage,omitempty"`
+ Usage *llm.Usage `json:"usage,omitempty"`
// Message timing information
StartTime *time.Time `json:"start_time,omitempty"`
@@ -164,7 +165,7 @@
}
// SetConvo sets m.ConversationID and m.ParentConversationID based on convo.
-func (m *AgentMessage) SetConvo(convo *ant.Convo) {
+func (m *AgentMessage) SetConvo(convo *conversation.Convo) {
if convo == nil {
m.ConversationID = ""
m.ParentConversationID = nil
@@ -262,16 +263,16 @@
// ConvoInterface defines the interface for conversation interactions
type ConvoInterface interface {
- CumulativeUsage() ant.CumulativeUsage
- ResetBudget(ant.Budget)
+ CumulativeUsage() conversation.CumulativeUsage
+ ResetBudget(conversation.Budget)
OverBudget() error
- SendMessage(message ant.Message) (*ant.MessageResponse, error)
- SendUserTextMessage(s string, otherContents ...ant.Content) (*ant.MessageResponse, error)
+ SendMessage(message llm.Message) (*llm.Response, error)
+ SendUserTextMessage(s string, otherContents ...llm.Content) (*llm.Response, error)
GetID() string
- ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error)
- ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error)
+ ToolResultContents(ctx context.Context, resp *llm.Response) ([]llm.Content, error)
+ ToolResultCancelContents(resp *llm.Response) ([]llm.Content, error)
CancelToolUse(toolUseID string, cause error) error
- SubConvoWithHistory() *ant.Convo
+ SubConvoWithHistory() *conversation.Convo
}
type Agent struct {
@@ -287,7 +288,7 @@
outsideHTTP string // base address of the outside webserver (only when under docker)
ready chan struct{} // closed when the agent is initialized (only when under docker)
startedAt time.Time
- originalBudget ant.Budget
+ originalBudget conversation.Budget
title string
branchName string
codereview *claudetool.CodeReviewer
@@ -531,7 +532,7 @@
}
// OnToolCall implements ant.Listener and tracks the start of a tool call.
-func (a *Agent) OnToolCall(ctx context.Context, convo *ant.Convo, id string, toolName string, toolInput json.RawMessage, content ant.Content) {
+func (a *Agent) OnToolCall(ctx context.Context, convo *conversation.Convo, id string, toolName string, toolInput json.RawMessage, content llm.Content) {
// Track the tool call
a.mu.Lock()
a.outstandingToolCalls[id] = toolName
@@ -539,7 +540,7 @@
}
// OnToolResult implements ant.Listener.
-func (a *Agent) OnToolResult(ctx context.Context, convo *ant.Convo, toolID string, toolName string, toolInput json.RawMessage, content ant.Content, result *string, err error) {
+func (a *Agent) OnToolResult(ctx context.Context, convo *conversation.Convo, toolID string, toolName string, toolInput json.RawMessage, content llm.Content, result *string, err error) {
// Remove the tool call from outstanding calls
a.mu.Lock()
delete(a.outstandingToolCalls, toolID)
@@ -553,13 +554,13 @@
ToolName: toolName,
ToolInput: string(toolInput),
ToolCallId: content.ToolUseID,
- StartTime: content.StartTime,
- EndTime: content.EndTime,
+ StartTime: content.ToolUseStartTime,
+ EndTime: content.ToolUseEndTime,
}
// Calculate the elapsed time if both start and end times are set
- if content.StartTime != nil && content.EndTime != nil {
- elapsed := content.EndTime.Sub(*content.StartTime)
+ if content.ToolUseStartTime != nil && content.ToolUseEndTime != nil {
+ elapsed := content.ToolUseEndTime.Sub(*content.ToolUseStartTime)
m.Elapsed = &elapsed
}
@@ -568,18 +569,18 @@
}
// OnRequest implements ant.Listener.
-func (a *Agent) OnRequest(ctx context.Context, convo *ant.Convo, id string, msg *ant.Message) {
+func (a *Agent) OnRequest(ctx context.Context, convo *conversation.Convo, id string, msg *llm.Message) {
a.mu.Lock()
defer a.mu.Unlock()
a.outstandingLLMCalls[id] = struct{}{}
// We already get tool results from the above. We send user messages to the outbox in the agent loop.
}
-// OnResponse implements ant.Listener. Responses contain messages from the LLM
+// OnResponse implements conversation.Listener. Responses contain messages from the LLM
// that need to be displayed (as well as tool calls that we send along when
// they're done). (It would be reasonable to also mention tool calls when they're
// started, but we don't do that yet.)
-func (a *Agent) OnResponse(ctx context.Context, convo *ant.Convo, id string, resp *ant.MessageResponse) {
+func (a *Agent) OnResponse(ctx context.Context, convo *conversation.Convo, id string, resp *llm.Response) {
// Remove the LLM call from outstanding calls
a.mu.Lock()
delete(a.outstandingLLMCalls, id)
@@ -597,7 +598,7 @@
}
endOfTurn := false
- if resp.StopReason != ant.StopReasonToolUse && convo.Parent == nil {
+ if resp.StopReason != llm.StopReasonToolUse && convo.Parent == nil {
endOfTurn = true
}
m := AgentMessage{
@@ -610,10 +611,10 @@
}
// Extract any tool calls from the response
- if resp.StopReason == ant.StopReasonToolUse {
+ if resp.StopReason == llm.StopReasonToolUse {
var toolCalls []ToolCall
for _, part := range resp.Content {
- if part.Type == ant.ContentTypeToolUse {
+ if part.Type == llm.ContentTypeToolUse {
toolCalls = append(toolCalls, ToolCall{
Name: part.ToolName,
Input: string(part.ToolInput),
@@ -653,17 +654,15 @@
return slices.Clone(a.history[start:end])
}
-func (a *Agent) OriginalBudget() ant.Budget {
+func (a *Agent) OriginalBudget() conversation.Budget {
return a.originalBudget
}
// AgentConfig contains configuration for creating a new Agent.
type AgentConfig struct {
Context context.Context
- AntURL string
- APIKey string
- HTTPC *http.Client
- Budget ant.Budget
+ Service llm.Service
+ Budget conversation.Budget
GitUsername string
GitEmail string
SessionID string
@@ -778,15 +777,9 @@
// initConvo initializes the conversation.
// It must not be called until all agent fields are initialized,
// particularly workingDir and git.
-func (a *Agent) initConvo() *ant.Convo {
+func (a *Agent) initConvo() *conversation.Convo {
ctx := a.config.Context
- convo := ant.NewConvo(ctx, a.config.APIKey)
- if a.config.HTTPC != nil {
- convo.HTTPC = a.config.HTTPC
- }
- if a.config.AntURL != "" {
- convo.URL = a.config.AntURL
- }
+ convo := conversation.New(ctx, a.config.Service)
convo.PromptCaching = true
convo.Budget = a.config.Budget
@@ -832,7 +825,7 @@
// Register all tools with the conversation
// When adding, removing, or modifying tools here, double-check that the termui tool display
// template in termui/termui.go has pretty-printing support for all tools.
- convo.Tools = []*ant.Tool{
+ convo.Tools = []*llm.Tool{
bashTool, claudetool.Keyword,
claudetool.Think, a.titleTool(), makeDoneTool(a.codereview, a.config.GitUsername, a.config.GitEmail),
a.codereview.Tool(),
@@ -863,8 +856,8 @@
return false
}
-func (a *Agent) titleTool() *ant.Tool {
- title := &ant.Tool{
+func (a *Agent) titleTool() *llm.Tool {
+ title := &llm.Tool{
Name: "title",
Description: `Sets the conversation title and creates a git branch for tracking work. MANDATORY: You must use this tool before making any git commits.`,
InputSchema: json.RawMessage(`{
@@ -990,20 +983,20 @@
}
}
-func (a *Agent) GatherMessages(ctx context.Context, block bool) ([]ant.Content, error) {
- var m []ant.Content
+func (a *Agent) GatherMessages(ctx context.Context, block bool) ([]llm.Content, error) {
+ var m []llm.Content
if block {
select {
case <-ctx.Done():
return m, ctx.Err()
case msg := <-a.inbox:
- m = append(m, ant.StringContent(msg))
+ m = append(m, llm.StringContent(msg))
}
}
for {
select {
case msg := <-a.inbox:
- m = append(m, ant.StringContent(msg))
+ m = append(m, llm.StringContent(msg))
default:
return m, nil
}
@@ -1052,7 +1045,7 @@
}
// If the model is not requesting to use a tool, we're done
- if resp.StopReason != ant.StopReasonToolUse {
+ if resp.StopReason != llm.StopReasonToolUse {
a.stateMachine.Transition(ctx, StateEndOfTurn, "LLM completed response, ending turn")
break
}
@@ -1078,7 +1071,7 @@
}
// processUserMessage waits for user messages and sends them to the model
-func (a *Agent) processUserMessage(ctx context.Context) (*ant.MessageResponse, error) {
+func (a *Agent) processUserMessage(ctx context.Context) (*llm.Response, error) {
// Wait for at least one message from the user
msgs, err := a.GatherMessages(ctx, true)
if err != nil { // e.g. the context was canceled while blocking in GatherMessages
@@ -1086,8 +1079,8 @@
return nil, err
}
- userMessage := ant.Message{
- Role: ant.MessageRoleUser,
+ userMessage := llm.Message{
+ Role: llm.MessageRoleUser,
Content: msgs,
}
@@ -1109,8 +1102,8 @@
}
// handleToolExecution processes a tool use request from the model
-func (a *Agent) handleToolExecution(ctx context.Context, resp *ant.MessageResponse) (bool, *ant.MessageResponse) {
- var results []ant.Content
+func (a *Agent) handleToolExecution(ctx context.Context, resp *llm.Response) (bool, *llm.Response) {
+ var results []llm.Content
cancelled := false
// Transition to checking for cancellation state
@@ -1200,7 +1193,7 @@
}
// continueTurnWithToolResults continues the conversation with tool results
-func (a *Agent) continueTurnWithToolResults(ctx context.Context, results []ant.Content, autoqualityMessages []string, cancelled bool) (bool, *ant.MessageResponse) {
+func (a *Agent) continueTurnWithToolResults(ctx context.Context, results []llm.Content, autoqualityMessages []string, cancelled bool) (bool, *llm.Response) {
// Get any messages the user sent while tools were executing
a.stateMachine.Transition(ctx, StateGatheringAdditionalMessages, "Gathering additional user messages")
msgs, err := a.GatherMessages(ctx, false)
@@ -1211,19 +1204,19 @@
// Inject any auto-generated messages from quality checks
for _, msg := range autoqualityMessages {
- msgs = append(msgs, ant.StringContent(msg))
+ msgs = append(msgs, llm.StringContent(msg))
}
// Handle cancellation by appending a message about it
if cancelled {
- msgs = append(msgs, ant.StringContent(cancelToolUseMessage))
+ msgs = append(msgs, llm.StringContent(cancelToolUseMessage))
// EndOfTurn is false here so that the client of this agent keeps processing
// further messages; the conversation is not over.
a.pushToOutbox(ctx, AgentMessage{Type: ErrorMessageType, Content: userCancelMessage, EndOfTurn: false})
} else if err := a.convo.OverBudget(); err != nil {
// Handle budget issues by appending a message about it
budgetMsg := "We've exceeded our budget. Please ask the user to confirm before continuing by ending the turn."
- msgs = append(msgs, ant.StringContent(budgetMsg))
+ msgs = append(msgs, llm.StringContent(budgetMsg))
a.pushToOutbox(ctx, budgetMessage(fmt.Errorf("warning: %w (ask to keep trying, if you'd like)", err)))
}
@@ -1232,8 +1225,8 @@
// Send the combined message to continue the conversation
a.stateMachine.Transition(ctx, StateSendingToolResults, "Sending tool results back to LLM")
- resp, err := a.convo.SendMessage(ant.Message{
- Role: ant.MessageRoleUser,
+ resp, err := a.convo.SendMessage(llm.Message{
+ Role: llm.MessageRoleUser,
Content: results,
})
if err != nil {
@@ -1264,11 +1257,11 @@
return nil
}
-func collectTextContent(msg *ant.MessageResponse) string {
+func collectTextContent(msg *llm.Response) string {
// Collect all text content
var allText strings.Builder
for _, content := range msg.Content {
- if content.Type == ant.ContentTypeText && content.Text != "" {
+ if content.Type == llm.ContentTypeText && content.Text != "" {
if allText.Len() > 0 {
allText.WriteString("\n\n")
}
@@ -1278,7 +1271,7 @@
return allText.String()
}
-func (a *Agent) TotalUsage() ant.CumulativeUsage {
+func (a *Agent) TotalUsage() conversation.CumulativeUsage {
a.mu.Lock()
defer a.mu.Unlock()
return a.convo.CumulativeUsage()
@@ -1604,7 +1597,7 @@
Reply with ONLY the reprompt text.
`
- userMessage := ant.UserStringMessage(msg)
+ userMessage := llm.UserStringMessage(msg)
// By doing this in a subconversation, the agent doesn't call tools (because
// there aren't any), and there's not a concurrency risk with on-going other
// outstanding conversations.
diff --git a/loop/agent_test.go b/loop/agent_test.go
index 0924b39..56708e3 100644
--- a/loop/agent_test.go
+++ b/loop/agent_test.go
@@ -11,8 +11,10 @@
"testing"
"time"
- "sketch.dev/ant"
"sketch.dev/httprr"
+ "sketch.dev/llm"
+ "sketch.dev/llm/ant"
+ "sketch.dev/llm/conversation"
)
// TestAgentLoop tests that the Agent loop functionality works correctly.
@@ -58,7 +60,7 @@
if err := os.Chdir("/"); err != nil {
t.Fatal(err)
}
- budget := ant.Budget{MaxResponses: 100}
+ budget := conversation.Budget{MaxResponses: 100}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
@@ -66,9 +68,11 @@
apiKey := cmp.Or(os.Getenv("OUTER_SKETCH_ANTHROPIC_API_KEY"), os.Getenv("ANTHROPIC_API_KEY"))
cfg := AgentConfig{
- Context: ctx,
- APIKey: apiKey,
- HTTPC: client,
+ Context: ctx,
+ Service: &ant.Service{
+ APIKey: apiKey,
+ HTTPC: client,
+ },
Budget: budget,
GitUsername: "Test Agent",
GitEmail: "totallyhuman@sketch.dev",
@@ -206,7 +210,7 @@
func TestAgentProcessTurnWithNilResponse(t *testing.T) {
// Create a mock conversation that will return nil and error
mockConvo := &MockConvoInterface{
- sendMessageFunc: func(message ant.Message) (*ant.MessageResponse, error) {
+ sendMessageFunc: func(message llm.Message) (*llm.Response, error) {
return nil, fmt.Errorf("test error: simulating nil response")
},
}
@@ -250,40 +254,40 @@
// MockConvoInterface implements the ConvoInterface for testing
type MockConvoInterface struct {
- sendMessageFunc func(message ant.Message) (*ant.MessageResponse, error)
- sendUserTextMessageFunc func(s string, otherContents ...ant.Content) (*ant.MessageResponse, error)
- toolResultContentsFunc func(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error)
- toolResultCancelContentsFunc func(resp *ant.MessageResponse) ([]ant.Content, error)
+ sendMessageFunc func(message llm.Message) (*llm.Response, error)
+ sendUserTextMessageFunc func(s string, otherContents ...llm.Content) (*llm.Response, error)
+ toolResultContentsFunc func(ctx context.Context, resp *llm.Response) ([]llm.Content, error)
+ toolResultCancelContentsFunc func(resp *llm.Response) ([]llm.Content, error)
cancelToolUseFunc func(toolUseID string, cause error) error
- cumulativeUsageFunc func() ant.CumulativeUsage
- resetBudgetFunc func(ant.Budget)
+ cumulativeUsageFunc func() conversation.CumulativeUsage
+ resetBudgetFunc func(conversation.Budget)
overBudgetFunc func() error
getIDFunc func() string
- subConvoWithHistoryFunc func() *ant.Convo
+ subConvoWithHistoryFunc func() *conversation.Convo
}
-func (m *MockConvoInterface) SendMessage(message ant.Message) (*ant.MessageResponse, error) {
+func (m *MockConvoInterface) SendMessage(message llm.Message) (*llm.Response, error) {
if m.sendMessageFunc != nil {
return m.sendMessageFunc(message)
}
return nil, nil
}
-func (m *MockConvoInterface) SendUserTextMessage(s string, otherContents ...ant.Content) (*ant.MessageResponse, error) {
+func (m *MockConvoInterface) SendUserTextMessage(s string, otherContents ...llm.Content) (*llm.Response, error) {
if m.sendUserTextMessageFunc != nil {
return m.sendUserTextMessageFunc(s, otherContents...)
}
return nil, nil
}
-func (m *MockConvoInterface) ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvoInterface) ToolResultContents(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
if m.toolResultContentsFunc != nil {
return m.toolResultContentsFunc(ctx, resp)
}
return nil, nil
}
-func (m *MockConvoInterface) ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvoInterface) ToolResultCancelContents(resp *llm.Response) ([]llm.Content, error) {
if m.toolResultCancelContentsFunc != nil {
return m.toolResultCancelContentsFunc(resp)
}
@@ -297,14 +301,14 @@
return nil
}
-func (m *MockConvoInterface) CumulativeUsage() ant.CumulativeUsage {
+func (m *MockConvoInterface) CumulativeUsage() conversation.CumulativeUsage {
if m.cumulativeUsageFunc != nil {
return m.cumulativeUsageFunc()
}
- return ant.CumulativeUsage{}
+ return conversation.CumulativeUsage{}
}
-func (m *MockConvoInterface) ResetBudget(budget ant.Budget) {
+func (m *MockConvoInterface) ResetBudget(budget conversation.Budget) {
if m.resetBudgetFunc != nil {
m.resetBudgetFunc(budget)
}
@@ -324,7 +328,7 @@
return "mock-convo-id"
}
-func (m *MockConvoInterface) SubConvoWithHistory() *ant.Convo {
+func (m *MockConvoInterface) SubConvoWithHistory() *conversation.Convo {
if m.subConvoWithHistoryFunc != nil {
return m.subConvoWithHistoryFunc()
}
@@ -337,7 +341,7 @@
func TestAgentProcessTurnWithNilResponseNilError(t *testing.T) {
// Create a mock conversation that will return nil response and nil error
mockConvo := &MockConvoInterface{
- sendMessageFunc: func(message ant.Message) (*ant.MessageResponse, error) {
+ sendMessageFunc: func(message llm.Message) (*llm.Response, error) {
return nil, nil // This is unusual but now handled gracefully
},
}
@@ -464,48 +468,48 @@
// mockConvoInterface is a mock implementation of ConvoInterface for testing
type mockConvoInterface struct {
- SendMessageFunc func(message ant.Message) (*ant.MessageResponse, error)
- ToolResultContentsFunc func(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error)
+ SendMessageFunc func(message llm.Message) (*llm.Response, error)
+ ToolResultContentsFunc func(ctx context.Context, resp *llm.Response) ([]llm.Content, error)
}
func (c *mockConvoInterface) GetID() string {
return "mockConvoInterface-id"
}
-func (c *mockConvoInterface) SubConvoWithHistory() *ant.Convo {
+func (c *mockConvoInterface) SubConvoWithHistory() *conversation.Convo {
return nil
}
-func (m *mockConvoInterface) CumulativeUsage() ant.CumulativeUsage {
- return ant.CumulativeUsage{}
+func (m *mockConvoInterface) CumulativeUsage() conversation.CumulativeUsage {
+ return conversation.CumulativeUsage{}
}
-func (m *mockConvoInterface) ResetBudget(ant.Budget) {}
+func (m *mockConvoInterface) ResetBudget(conversation.Budget) {}
func (m *mockConvoInterface) OverBudget() error {
return nil
}
-func (m *mockConvoInterface) SendMessage(message ant.Message) (*ant.MessageResponse, error) {
+func (m *mockConvoInterface) SendMessage(message llm.Message) (*llm.Response, error) {
if m.SendMessageFunc != nil {
return m.SendMessageFunc(message)
}
- return &ant.MessageResponse{StopReason: ant.StopReasonEndTurn}, nil
+ return &llm.Response{StopReason: llm.StopReasonEndTurn}, nil
}
-func (m *mockConvoInterface) SendUserTextMessage(s string, otherContents ...ant.Content) (*ant.MessageResponse, error) {
- return m.SendMessage(ant.UserStringMessage(s))
+func (m *mockConvoInterface) SendUserTextMessage(s string, otherContents ...llm.Content) (*llm.Response, error) {
+ return m.SendMessage(llm.UserStringMessage(s))
}
-func (m *mockConvoInterface) ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *mockConvoInterface) ToolResultContents(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
if m.ToolResultContentsFunc != nil {
return m.ToolResultContentsFunc(ctx, resp)
}
- return []ant.Content{}, nil
+ return []llm.Content{}, nil
}
-func (m *mockConvoInterface) ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error) {
- return []ant.Content{ant.StringContent("Tool use cancelled")}, nil
+func (m *mockConvoInterface) ToolResultCancelContents(resp *llm.Response) ([]llm.Content, error) {
+ return []llm.Content{llm.StringContent("Tool use cancelled")}, nil
}
func (m *mockConvoInterface) CancelToolUse(toolUseID string, cause error) error {
@@ -542,11 +546,11 @@
agent.inbox <- "Test message"
// Setup the mock to simulate a model response with end of turn
- mockConvo.SendMessageFunc = func(message ant.Message) (*ant.MessageResponse, error) {
- return &ant.MessageResponse{
- StopReason: ant.StopReasonEndTurn,
- Content: []ant.Content{
- ant.StringContent("This is a test response"),
+ mockConvo.SendMessageFunc = func(message llm.Message) (*llm.Response, error) {
+ return &llm.Response{
+ StopReason: llm.StopReasonEndTurn,
+ Content: []llm.Content{
+ llm.StringContent("This is a test response"),
},
}, nil
}
@@ -615,29 +619,29 @@
// First response requests a tool
firstResponseDone := false
- mockConvo.SendMessageFunc = func(message ant.Message) (*ant.MessageResponse, error) {
+ mockConvo.SendMessageFunc = func(message llm.Message) (*llm.Response, error) {
if !firstResponseDone {
firstResponseDone = true
- return &ant.MessageResponse{
- StopReason: ant.StopReasonToolUse,
- Content: []ant.Content{
- ant.StringContent("I'll use a tool"),
- {Type: ant.ContentTypeToolUse, ToolName: "test_tool", ToolInput: []byte("{}"), ID: "test_id"},
+ return &llm.Response{
+ StopReason: llm.StopReasonToolUse,
+ Content: []llm.Content{
+ llm.StringContent("I'll use a tool"),
+ {Type: llm.ContentTypeToolUse, ToolName: "test_tool", ToolInput: []byte("{}"), ID: "test_id"},
},
}, nil
}
// Second response ends the turn
- return &ant.MessageResponse{
- StopReason: ant.StopReasonEndTurn,
- Content: []ant.Content{
- ant.StringContent("Finished using the tool"),
+ return &llm.Response{
+ StopReason: llm.StopReasonEndTurn,
+ Content: []llm.Content{
+ llm.StringContent("Finished using the tool"),
},
}, nil
}
// Tool result content handler
- mockConvo.ToolResultContentsFunc = func(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
- return []ant.Content{ant.StringContent("Tool executed successfully")}, nil
+ mockConvo.ToolResultContentsFunc = func(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
+ return []llm.Content{llm.StringContent("Tool executed successfully")}, nil
}
// Track state transitions
diff --git a/loop/donetool.go b/loop/donetool.go
index e4b0542..63604d8 100644
--- a/loop/donetool.go
+++ b/loop/donetool.go
@@ -5,8 +5,8 @@
"encoding/json"
"fmt"
- "sketch.dev/ant"
"sketch.dev/claudetool"
+ "sketch.dev/llm"
)
// makeDoneTool creates a tool that provides a checklist to the agent. There
@@ -14,8 +14,8 @@
// not as reliable as it could be. Historically, we've found that Claude ignores
// the tool results here, so we don't tell the tool to say "hey, really check this"
// at the moment, though we've tried.
-func makeDoneTool(codereview *claudetool.CodeReviewer, gitUsername, gitEmail string) *ant.Tool {
- return &ant.Tool{
+func makeDoneTool(codereview *claudetool.CodeReviewer, gitUsername, gitEmail string) *llm.Tool {
+ return &llm.Tool{
Name: "done",
Description: `Use this tool when you have achieved the user's goal. The parameters form a checklist which you should evaluate.`,
InputSchema: json.RawMessage(doneChecklistJSONSchema(gitUsername, gitEmail)),
diff --git a/loop/mocks.go b/loop/mocks.go
index 7e05070..811ab2c 100644
--- a/loop/mocks.go
+++ b/loop/mocks.go
@@ -6,10 +6,11 @@
"sync"
"testing"
- "sketch.dev/ant"
+ "sketch.dev/llm"
+ "sketch.dev/llm/conversation"
)
-// MockConvo is a custom mock for ant.Convo interface
+// MockConvo is a custom mock for conversation.Convo interface
type MockConvo struct {
mu sync.Mutex
t *testing.T
@@ -21,23 +22,23 @@
}
type mockCall struct {
- args []interface{}
- result []interface{}
+ args []any
+ result []any
}
type mockExpectation struct {
until chan any
- args []interface{}
- result []interface{}
+ args []any
+ result []any
}
// Return sets up return values for an expectation
-func (e *mockExpectation) Return(values ...interface{}) {
+func (e *mockExpectation) Return(values ...any) {
e.result = values
}
// Return sets up return values for an expectation
-func (e *mockExpectation) BlockAndReturn(until chan any, values ...interface{}) {
+func (e *mockExpectation) BlockAndReturn(until chan any, values ...any) {
e.until = until
e.result = values
}
@@ -53,7 +54,7 @@
}
// ExpectCall sets up an expectation for a method call
-func (m *MockConvo) ExpectCall(method string, args ...interface{}) *mockExpectation {
+func (m *MockConvo) ExpectCall(method string, args ...any) *mockExpectation {
m.mu.Lock()
defer m.mu.Unlock()
expectation := &mockExpectation{args: args}
@@ -65,7 +66,7 @@
}
// findMatchingExpectation finds a matching expectation for a method call
-func (m *MockConvo) findMatchingExpectation(method string, args ...interface{}) (*mockExpectation, bool) {
+func (m *MockConvo) findMatchingExpectation(method string, args ...any) (*mockExpectation, bool) {
m.mu.Lock()
defer m.mu.Unlock()
expectations, ok := m.expectations[method]
@@ -87,7 +88,7 @@
}
// matchArgs checks if call arguments match expectation arguments
-func matchArgs(expected, actual []interface{}) bool {
+func matchArgs(expected, actual []any) bool {
if len(expected) != len(actual) {
return false
}
@@ -107,7 +108,7 @@
}
// recordCall records a method call
-func (m *MockConvo) recordCall(method string, args ...interface{}) {
+func (m *MockConvo) recordCall(method string, args ...any) {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.calls[method]; !ok {
@@ -116,7 +117,7 @@
m.calls[method] = append(m.calls[method], &mockCall{args: args})
}
-func (m *MockConvo) SendMessage(message ant.Message) (*ant.MessageResponse, error) {
+func (m *MockConvo) SendMessage(message llm.Message) (*llm.Response, error) {
m.recordCall("SendMessage", message)
exp, ok := m.findMatchingExpectation("SendMessage", message)
if !ok {
@@ -129,10 +130,10 @@
if err, ok := exp.result[1].(error); ok {
retErr = err
}
- return exp.result[0].(*ant.MessageResponse), retErr
+ return exp.result[0].(*llm.Response), retErr
}
-func (m *MockConvo) SendUserTextMessage(message string, otherContents ...ant.Content) (*ant.MessageResponse, error) {
+func (m *MockConvo) SendUserTextMessage(message string, otherContents ...llm.Content) (*llm.Response, error) {
m.recordCall("SendUserTextMessage", message, otherContents)
exp, ok := m.findMatchingExpectation("SendUserTextMessage", message, otherContents)
if !ok {
@@ -145,10 +146,10 @@
if err, ok := exp.result[1].(error); ok {
retErr = err
}
- return exp.result[0].(*ant.MessageResponse), retErr
+ return exp.result[0].(*llm.Response), retErr
}
-func (m *MockConvo) ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvo) ToolResultContents(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
m.recordCall("ToolResultContents", resp)
exp, ok := m.findMatchingExpectation("ToolResultContents", resp)
if !ok {
@@ -162,10 +163,10 @@
retErr = err
}
- return exp.result[0].([]ant.Content), retErr
+ return exp.result[0].([]llm.Content), retErr
}
-func (m *MockConvo) ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvo) ToolResultCancelContents(resp *llm.Response) ([]llm.Content, error) {
m.recordCall("ToolResultCancelContents", resp)
exp, ok := m.findMatchingExpectation("ToolResultCancelContents", resp)
if !ok {
@@ -179,12 +180,12 @@
retErr = err
}
- return exp.result[0].([]ant.Content), retErr
+ return exp.result[0].([]llm.Content), retErr
}
-func (m *MockConvo) CumulativeUsage() ant.CumulativeUsage {
+func (m *MockConvo) CumulativeUsage() conversation.CumulativeUsage {
m.recordCall("CumulativeUsage")
- return ant.CumulativeUsage{}
+ return conversation.CumulativeUsage{}
}
func (m *MockConvo) OverBudget() error {
@@ -197,12 +198,12 @@
return "mock-conversation-id"
}
-func (m *MockConvo) SubConvoWithHistory() *ant.Convo {
+func (m *MockConvo) SubConvoWithHistory() *conversation.Convo {
m.recordCall("SubConvoWithHistory")
return nil
}
-func (m *MockConvo) ResetBudget(_ ant.Budget) {
+func (m *MockConvo) ResetBudget(_ conversation.Budget) {
m.recordCall("ResetBudget")
}
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 4a415c8..f7a3979 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -23,7 +23,7 @@
"sketch.dev/loop/server/gzhandler"
"github.com/creack/pty"
- "sketch.dev/ant"
+ "sketch.dev/llm/conversation"
"sketch.dev/loop"
"sketch.dev/webui"
)
@@ -50,29 +50,29 @@
}
type State struct {
- MessageCount int `json:"message_count"`
- TotalUsage *ant.CumulativeUsage `json:"total_usage,omitempty"`
- InitialCommit string `json:"initial_commit"`
- Title string `json:"title"`
- BranchName string `json:"branch_name,omitempty"`
- Hostname string `json:"hostname"` // deprecated
- WorkingDir string `json:"working_dir"` // deprecated
- OS string `json:"os"` // deprecated
- GitOrigin string `json:"git_origin,omitempty"`
- OutstandingLLMCalls int `json:"outstanding_llm_calls"`
- OutstandingToolCalls []string `json:"outstanding_tool_calls"`
- SessionID string `json:"session_id"`
- SSHAvailable bool `json:"ssh_available"`
- SSHError string `json:"ssh_error,omitempty"`
- InContainer bool `json:"in_container"`
- FirstMessageIndex int `json:"first_message_index"`
- AgentState string `json:"agent_state,omitempty"`
- OutsideHostname string `json:"outside_hostname,omitempty"`
- InsideHostname string `json:"inside_hostname,omitempty"`
- OutsideOS string `json:"outside_os,omitempty"`
- InsideOS string `json:"inside_os,omitempty"`
- OutsideWorkingDir string `json:"outside_working_dir,omitempty"`
- InsideWorkingDir string `json:"inside_working_dir,omitempty"`
+ MessageCount int `json:"message_count"`
+ TotalUsage *conversation.CumulativeUsage `json:"total_usage,omitempty"`
+ InitialCommit string `json:"initial_commit"`
+ Title string `json:"title"`
+ BranchName string `json:"branch_name,omitempty"`
+ Hostname string `json:"hostname"` // deprecated
+ WorkingDir string `json:"working_dir"` // deprecated
+ OS string `json:"os"` // deprecated
+ GitOrigin string `json:"git_origin,omitempty"`
+ OutstandingLLMCalls int `json:"outstanding_llm_calls"`
+ OutstandingToolCalls []string `json:"outstanding_tool_calls"`
+ SessionID string `json:"session_id"`
+ SSHAvailable bool `json:"ssh_available"`
+ SSHError string `json:"ssh_error,omitempty"`
+ InContainer bool `json:"in_container"`
+ FirstMessageIndex int `json:"first_message_index"`
+ AgentState string `json:"agent_state,omitempty"`
+ OutsideHostname string `json:"outside_hostname,omitempty"`
+ InsideHostname string `json:"inside_hostname,omitempty"`
+ OutsideOS string `json:"outside_os,omitempty"`
+ InsideOS string `json:"inside_os,omitempty"`
+ OutsideWorkingDir string `json:"outside_working_dir,omitempty"`
+ InsideWorkingDir string `json:"inside_working_dir,omitempty"`
}
type InitRequest struct {
@@ -298,12 +298,12 @@
// Create a combined structure with all information
downloadData := struct {
- Messages []loop.AgentMessage `json:"messages"`
- MessageCount int `json:"message_count"`
- TotalUsage ant.CumulativeUsage `json:"total_usage"`
- Hostname string `json:"hostname"`
- WorkingDir string `json:"working_dir"`
- DownloadTime string `json:"download_time"`
+ Messages []loop.AgentMessage `json:"messages"`
+ MessageCount int `json:"message_count"`
+ TotalUsage conversation.CumulativeUsage `json:"total_usage"`
+ Hostname string `json:"hostname"`
+ WorkingDir string `json:"working_dir"`
+ DownloadTime string `json:"download_time"`
}{
Messages: messages,
MessageCount: messageCount,