Merge branch 'main' into fix-formatting

# Conflicts:
#	pkg/agent/loop.go
#	pkg/agent/loop_test.go
#	pkg/channels/discord.go
#	pkg/channels/onebot.go
#	pkg/config/config.go
#	pkg/tools/subagent_tool_test.go
This commit is contained in:
Artem Yadelskyi
2026-02-19 22:04:48 +02:00
28 changed files with 2245 additions and 661 deletions
+15 -1
View File
@@ -21,6 +21,8 @@ type AgentInstance struct {
Fallbacks []string
Workspace string
MaxIterations int
MaxTokens int
Temperature float64
ContextWindow int
Provider providers.LLMProvider
Sessions *session.SessionManager
@@ -76,6 +78,16 @@ func NewAgentInstance(
maxIter = 20
}
maxTokens := defaults.MaxTokens
if maxTokens == 0 {
maxTokens = 8192
}
temperature := 0.7
if defaults.Temperature != nil {
temperature = *defaults.Temperature
}
// Resolve fallback candidates
modelCfg := providers.ModelConfig{
Primary: model,
@@ -90,7 +102,9 @@ func NewAgentInstance(
Fallbacks: fallbacks,
Workspace: workspace,
MaxIterations: maxIter,
ContextWindow: defaults.MaxTokens,
MaxTokens: maxTokens,
Temperature: temperature,
ContextWindow: maxTokens,
Provider: provider,
Sessions: sessionsManager,
ContextBuilder: contextBuilder,
+95
View File
@@ -0,0 +1,95 @@
package agent
import (
"os"
"testing"
"github.com/sipeed/picoclaw/pkg/config"
)
func TestNewAgentInstance_UsesDefaultsTemperatureAndMaxTokens(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 1234,
MaxToolIterations: 5,
},
},
}
configuredTemp := 1.0
cfg.Agents.Defaults.Temperature = &configuredTemp
provider := &mockProvider{}
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
if agent.MaxTokens != 1234 {
t.Fatalf("MaxTokens = %d, want %d", agent.MaxTokens, 1234)
}
if agent.Temperature != 1.0 {
t.Fatalf("Temperature = %f, want %f", agent.Temperature, 1.0)
}
}
func TestNewAgentInstance_DefaultsTemperatureWhenZero(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 1234,
MaxToolIterations: 5,
},
},
}
configuredTemp := 0.0
cfg.Agents.Defaults.Temperature = &configuredTemp
provider := &mockProvider{}
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
if agent.Temperature != 0.0 {
t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.0)
}
}
func TestNewAgentInstance_DefaultsTemperatureWhenUnset(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-instance-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 1234,
MaxToolIterations: 5,
},
},
}
provider := &mockProvider{}
agent := NewAgentInstance(nil, &cfg.Agents.Defaults, cfg, provider)
if agent.Temperature != 0.7 {
t.Fatalf("Temperature = %f, want %f", agent.Temperature, 0.7)
}
}
+48 -78
View File
@@ -79,9 +79,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
}
// registerSharedTools registers tools that are shared across all agents (web, message, spawn).
func registerSharedTools(
cfg *config.Config, msgBus *bus.MessageBus, registry *AgentRegistry, provider providers.LLMProvider,
) {
func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *AgentRegistry, provider providers.LLMProvider) {
for _, agentID := range registry.ListAgentIDs() {
agent, ok := registry.GetAgent(agentID)
if !ok {
@@ -121,6 +119,7 @@ func registerSharedTools(
// Spawn tool with allowlist checker
subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus)
subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
spawnTool := tools.NewSpawnTool(subagentManager)
currentAgentID := agentID
spawnTool.SetAllowlistChecker(func(targetAgentID string) bool {
@@ -217,9 +216,7 @@ func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey stri
return al.ProcessDirectWithChannel(ctx, content, sessionKey, "cli", "direct")
}
func (al *AgentLoop) ProcessDirectWithChannel(
ctx context.Context, content, sessionKey, channel, chatID string,
) (string, error) {
func (al *AgentLoop) ProcessDirectWithChannel(ctx context.Context, content, sessionKey, channel, chatID string) (string, error) {
msg := bus.InboundMessage{
Channel: channel,
SenderID: "cron",
@@ -256,7 +253,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
logContent = utils.Truncate(msg.Content, 80)
}
logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, logContent),
map[string]any{
map[string]interface{}{
"channel": msg.Channel,
"chat_id": msg.ChatID,
"sender_id": msg.SenderID,
@@ -295,7 +292,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
}
logger.InfoCF("agent", "Routed message",
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"session_key": sessionKey,
"matched_by": route.MatchedBy,
@@ -318,7 +315,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
}
logger.InfoCF("agent", "Processing system message",
map[string]any{
map[string]interface{}{
"sender_id": msg.SenderID,
"chat_id": msg.ChatID,
})
@@ -343,7 +340,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
// Skip internal channels - only log, don't send to user
if constants.IsInternalChannel(originChannel) {
logger.InfoCF("agent", "Subagent completed (internal channel)",
map[string]any{
map[string]interface{}{
"sender_id": msg.SenderID,
"content_len": len(content),
"channel": originChannel,
@@ -376,7 +373,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
if !constants.IsInternalChannel(opts.Channel) {
channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID)
if err := al.RecordLastChannel(channelKey); err != nil {
logger.WarnCF("agent", "Failed to record last channel", map[string]any{"error": err.Error()})
logger.WarnCF("agent", "Failed to record last channel", map[string]interface{}{"error": err.Error()})
}
}
}
@@ -438,7 +435,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
// 9. Log response
responsePreview := utils.Truncate(finalContent, 120)
logger.InfoCF("agent", fmt.Sprintf("Response: %s", responsePreview),
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"session_key": opts.SessionKey,
"iterations": iteration,
@@ -449,9 +446,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
}
// runLLMIteration executes the LLM call loop with tool handling.
func (al *AgentLoop) runLLMIteration(
ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions,
) (string, int, error) {
func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions) (string, int, error) {
iteration := 0
var finalContent string
@@ -459,7 +454,7 @@ func (al *AgentLoop) runLLMIteration(
iteration++
logger.DebugCF("agent", "LLM iteration",
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"iteration": iteration,
"max": agent.MaxIterations,
@@ -470,20 +465,20 @@ func (al *AgentLoop) runLLMIteration(
// Log LLM request details
logger.DebugCF("agent", "LLM request",
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"iteration": iteration,
"model": agent.Model,
"messages_count": len(messages),
"tools_count": len(providerToolDefs),
"max_tokens": 8192,
"temperature": 0.7,
"max_tokens": agent.MaxTokens,
"temperature": agent.Temperature,
"system_prompt_len": len(messages[0].Content),
})
// Log full messages (detailed)
logger.DebugCF("agent", "Full LLM request",
map[string]any{
map[string]interface{}{
"iteration": iteration,
"messages_json": formatMessagesForLog(messages),
"tools_json": formatToolsForLog(providerToolDefs),
@@ -497,9 +492,9 @@ func (al *AgentLoop) runLLMIteration(
if len(agent.Candidates) > 1 && al.fallback != nil {
fbResult, fbErr := al.fallback.Execute(ctx, agent.Candidates,
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]any{
"max_tokens": 8192,
"temperature": 0.7,
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]interface{}{
"max_tokens": agent.MaxTokens,
"temperature": agent.Temperature,
})
},
)
@@ -509,13 +504,13 @@ func (al *AgentLoop) runLLMIteration(
if fbResult.Provider != "" && len(fbResult.Attempts) > 0 {
logger.InfoCF("agent", fmt.Sprintf("Fallback: succeeded with %s/%s after %d attempts",
fbResult.Provider, fbResult.Model, len(fbResult.Attempts)+1),
map[string]any{"agent_id": agent.ID, "iteration": iteration})
map[string]interface{}{"agent_id": agent.ID, "iteration": iteration})
}
return fbResult.Response, nil
}
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]any{
"max_tokens": 8192,
"temperature": 0.7,
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]interface{}{
"max_tokens": agent.MaxTokens,
"temperature": agent.Temperature,
})
}
@@ -534,7 +529,7 @@ func (al *AgentLoop) runLLMIteration(
strings.Contains(errMsg, "length")
if isContextError && retry < maxRetries {
logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]any{
logger.WarnCF("agent", "Context window error detected, attempting compression", map[string]interface{}{
"error": err.Error(),
"retry": retry,
})
@@ -561,7 +556,7 @@ func (al *AgentLoop) runLLMIteration(
if err != nil {
logger.ErrorCF("agent", "LLM call failed",
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"iteration": iteration,
"error": err.Error(),
@@ -573,7 +568,7 @@ func (al *AgentLoop) runLLMIteration(
if len(response.ToolCalls) == 0 {
finalContent = response.Content
logger.InfoCF("agent", "LLM response without tool calls (direct answer)",
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"iteration": iteration,
"content_chars": len(finalContent),
@@ -587,7 +582,7 @@ func (al *AgentLoop) runLLMIteration(
toolNames = append(toolNames, tc.Name)
}
logger.InfoCF("agent", "LLM requested tool calls",
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"tools": toolNames,
"count": len(response.ToolCalls),
@@ -608,6 +603,7 @@ func (al *AgentLoop) runLLMIteration(
Name: tc.Name,
Arguments: string(argumentsJSON),
},
Name: tc.Name,
})
}
messages = append(messages, assistantMsg)
@@ -620,7 +616,7 @@ func (al *AgentLoop) runLLMIteration(
argsJSON, _ := json.Marshal(tc.Arguments)
argsPreview := utils.Truncate(string(argsJSON), 200)
logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
map[string]any{
map[string]interface{}{
"agent_id": agent.ID,
"tool": tc.Name,
"iteration": iteration,
@@ -635,16 +631,14 @@ func (al *AgentLoop) runLLMIteration(
// The agent will handle user notification via processSystemMessage
if !result.Silent && result.ForUser != "" {
logger.InfoCF("agent", "Async tool completed, agent will handle notification",
map[string]any{
map[string]interface{}{
"tool": tc.Name,
"content_len": len(result.ForUser),
})
}
}
toolResult := agent.Tools.ExecuteWithContext(
ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback,
)
toolResult := agent.Tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback)
// Send ForUser content to user immediately if not Silent
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {
@@ -654,7 +648,7 @@ func (al *AgentLoop) runLLMIteration(
Content: toolResult.ForUser,
})
logger.DebugCF("agent", "Sent tool result to user",
map[string]any{
map[string]interface{}{
"tool": tc.Name,
"content_len": len(toolResult.ForUser),
})
@@ -760,10 +754,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
newHistory = append(newHistory, history[0]) // System prompt
// Add a note about compression
compressionNote := fmt.Sprintf(
"[System: Emergency compression dropped %d oldest messages due to context limit]",
droppedCount,
)
compressionNote := fmt.Sprintf("[System: Emergency compression dropped %d oldest messages due to context limit]", droppedCount)
// If there was an existing summary, we might lose it if it was in the dropped part (which is just messages).
// The summary is stored separately in session.Summary, so it persists!
// We just need to ensure the user knows there's a gap.
@@ -781,7 +772,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
agent.Sessions.SetHistory(sessionKey, newHistory)
agent.Sessions.Save(sessionKey)
logger.WarnCF("agent", "Forced compression executed", map[string]any{
logger.WarnCF("agent", "Forced compression executed", map[string]interface{}{
"session_key": sessionKey,
"dropped_msgs": droppedCount,
"new_count": len(newHistory),
@@ -789,8 +780,8 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
}
// GetStartupInfo returns information about loaded tools and skills for logging.
func (al *AgentLoop) GetStartupInfo() map[string]any {
info := make(map[string]any)
func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
info := make(map[string]interface{})
agent := al.registry.GetDefaultAgent()
if agent == nil {
@@ -799,7 +790,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]any {
// Tools info
toolsList := agent.Tools.List()
info["tools"] = map[string]any{
info["tools"] = map[string]interface{}{
"count": len(toolsList),
"names": toolsList,
}
@@ -808,7 +799,7 @@ func (al *AgentLoop) GetStartupInfo() map[string]any {
info["skills"] = agent.ContextBuilder.GetSkillsInfo()
// Agents info
info["agents"] = map[string]any{
info["agents"] = map[string]interface{}{
"count": len(al.registry.ListAgentIDs()),
"ids": al.registry.ListAgentIDs(),
}
@@ -860,10 +851,7 @@ func formatToolsForLog(tools []providers.ToolDefinition) string {
result += fmt.Sprintf(" [%d] Type: %s, Name: %s\n", i, tool.Type, tool.Function.Name)
result += fmt.Sprintf(" Description: %s\n", tool.Function.Description)
if len(tool.Function.Parameters) > 0 {
result += fmt.Sprintf(
" Parameters: %s\n",
utils.Truncate(fmt.Sprintf("%v", tool.Function.Parameters), 200),
)
result += fmt.Sprintf(" Parameters: %s\n", utils.Truncate(fmt.Sprintf("%v", tool.Function.Parameters), 200))
}
}
result += "]"
@@ -916,21 +904,11 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
s1, _ := al.summarizeBatch(ctx, agent, part1, "")
s2, _ := al.summarizeBatch(ctx, agent, part2, "")
mergePrompt := fmt.Sprintf(
"Merge these two conversation summaries into one cohesive summary:\n\n1: %s\n\n2: %s",
s1,
s2,
)
resp, err := agent.Provider.Chat(
ctx,
[]providers.Message{{Role: "user", Content: mergePrompt}},
nil,
agent.Model,
map[string]any{
"max_tokens": 1024,
"temperature": 0.3,
},
)
mergePrompt := fmt.Sprintf("Merge these two conversation summaries into one cohesive summary:\n\n1: %s\n\n2: %s", s1, s2)
resp, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: mergePrompt}}, nil, agent.Model, map[string]interface{}{
"max_tokens": 1024,
"temperature": 0.3,
})
if err == nil {
finalSummary = resp.Content
} else {
@@ -952,9 +930,7 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
}
// summarizeBatch summarizes a batch of messages.
func (al *AgentLoop) summarizeBatch(
ctx context.Context, agent *AgentInstance, batch []providers.Message, existingSummary string,
) (string, error) {
func (al *AgentLoop) summarizeBatch(ctx context.Context, agent *AgentInstance, batch []providers.Message, existingSummary string) (string, error) {
prompt := "Provide a concise summary of this conversation segment, preserving core context and key points.\n"
if existingSummary != "" {
prompt += "Existing context: " + existingSummary + "\n"
@@ -964,16 +940,10 @@ func (al *AgentLoop) summarizeBatch(
prompt += fmt.Sprintf("%s: %s\n", m.Role, m.Content)
}
response, err := agent.Provider.Chat(
ctx,
[]providers.Message{{Role: "user", Content: prompt}},
nil,
agent.Model,
map[string]any{
"max_tokens": 1024,
"temperature": 0.3,
},
)
response, err := agent.Provider.Chat(ctx, []providers.Message{{Role: "user", Content: prompt}}, nil, agent.Model, map[string]interface{}{
"max_tokens": 1024,
"temperature": 0.3,
})
if err != nil {
return "", err
}
+14 -43
View File
@@ -14,23 +14,6 @@ import (
"github.com/sipeed/picoclaw/pkg/tools"
)
// mockProvider is a simple mock LLM provider for testing
type mockProvider struct{}
func (m *mockProvider) Chat(
ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string,
opts map[string]any,
) (*providers.LLMResponse, error) {
return &providers.LLMResponse{
Content: "Mock response",
ToolCalls: []providers.ToolCall{},
}, nil
}
func (m *mockProvider) GetDefaultModel() string {
return "mock-model"
}
func TestRecordLastChannel(t *testing.T) {
// Create temp workspace
tmpDir, err := os.MkdirTemp("", "agent-test-*")
@@ -188,7 +171,7 @@ func TestToolRegistry_ToolRegistration(t *testing.T) {
// Verify tool is registered by checking it doesn't panic on GetStartupInfo
// (actual tool retrieval is tested in tools package tests)
info := al.GetStartupInfo()
toolsInfo := info["tools"].(map[string]any)
toolsInfo := info["tools"].(map[string]interface{})
toolsList := toolsInfo["names"].([]string)
// Check that our custom tool name is in the list
@@ -263,7 +246,7 @@ func TestToolRegistry_GetDefinitions(t *testing.T) {
al.RegisterTool(testTool)
info := al.GetStartupInfo()
toolsInfo := info["tools"].(map[string]any)
toolsInfo := info["tools"].(map[string]interface{})
toolsList := toolsInfo["names"].([]string)
// Check that our custom tool name is in the list
@@ -310,7 +293,7 @@ func TestAgentLoop_GetStartupInfo(t *testing.T) {
t.Fatal("Expected 'tools' key in startup info")
}
toolsMap, ok := toolsInfo.(map[string]any)
toolsMap, ok := toolsInfo.(map[string]interface{})
if !ok {
t.Fatal("Expected 'tools' to be a map")
}
@@ -366,10 +349,7 @@ type simpleMockProvider struct {
response string
}
func (m *simpleMockProvider) Chat(
ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string,
opts map[string]any,
) (*providers.LLMResponse, error) {
func (m *simpleMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
return &providers.LLMResponse{
Content: m.response,
ToolCalls: []providers.ToolCall{},
@@ -391,14 +371,14 @@ func (m *mockCustomTool) Description() string {
return "Mock custom tool for testing"
}
func (m *mockCustomTool) Parameters() map[string]any {
return map[string]any{
func (m *mockCustomTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]any{},
"properties": map[string]interface{}{},
}
}
func (m *mockCustomTool) Execute(ctx context.Context, args map[string]any) *tools.ToolResult {
func (m *mockCustomTool) Execute(ctx context.Context, args map[string]interface{}) *tools.ToolResult {
return tools.SilentResult("Custom tool executed")
}
@@ -416,14 +396,14 @@ func (m *mockContextualTool) Description() string {
return "Mock contextual tool"
}
func (m *mockContextualTool) Parameters() map[string]any {
return map[string]any{
func (m *mockContextualTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]any{},
"properties": map[string]interface{}{},
}
}
func (m *mockContextualTool) Execute(ctx context.Context, args map[string]any) *tools.ToolResult {
func (m *mockContextualTool) Execute(ctx context.Context, args map[string]interface{}) *tools.ToolResult {
return tools.SilentResult("Contextual tool executed")
}
@@ -543,10 +523,7 @@ type failFirstMockProvider struct {
successResp string
}
func (m *failFirstMockProvider) Chat(
ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string,
opts map[string]any,
) (*providers.LLMResponse, error) {
func (m *failFirstMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
m.currentCall++
if m.currentCall <= m.failures {
return nil, m.failError
@@ -611,13 +588,7 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
// Call ProcessDirectWithChannel
// Note: ProcessDirectWithChannel calls processMessage which will execute runLLMIteration
response, err := al.ProcessDirectWithChannel(
context.Background(),
"Trigger message",
sessionKey,
"test",
"test-chat",
)
response, err := al.ProcessDirectWithChannel(context.Background(), "Trigger message", sessionKey, "test", "test-chat")
if err != nil {
t.Fatalf("Expected success after retry, got error: %v", err)
}
+20
View File
@@ -0,0 +1,20 @@
package agent
import (
"context"
"github.com/sipeed/picoclaw/pkg/providers"
)
type mockProvider struct{}
func (m *mockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) {
return &providers.LLMResponse{
Content: "Mock response",
ToolCalls: []providers.ToolCall{},
}, nil
}
func (m *mockProvider) GetDefaultModel() string {
return "mock-model"
}