mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
merge: resolve conflicts with main
This commit is contained in:
+27
-12
@@ -3,6 +3,7 @@ package tools
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -107,13 +108,27 @@ func (r *ToolRegistry) ExecuteWithContext(
|
||||
return result
|
||||
}
|
||||
|
||||
// sortedToolNames returns tool names in sorted order for deterministic iteration.
|
||||
// This is critical for KV cache stability: non-deterministic map iteration would
|
||||
// produce different system prompts and tool definitions on each call, invalidating
|
||||
// the LLM's prefix cache even when no tools have changed.
|
||||
func (r *ToolRegistry) sortedToolNames() []string {
|
||||
names := make([]string, 0, len(r.tools))
|
||||
for name := range r.tools {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) GetDefinitions() []map[string]any {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
definitions := make([]map[string]any, 0, len(r.tools))
|
||||
for _, tool := range r.tools {
|
||||
definitions = append(definitions, ToolToSchema(tool))
|
||||
sorted := r.sortedToolNames()
|
||||
definitions := make([]map[string]any, 0, len(sorted))
|
||||
for _, name := range sorted {
|
||||
definitions = append(definitions, ToolToSchema(r.tools[name]))
|
||||
}
|
||||
return definitions
|
||||
}
|
||||
@@ -124,8 +139,10 @@ func (r *ToolRegistry) ToProviderDefs() []providers.ToolDefinition {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
definitions := make([]providers.ToolDefinition, 0, len(r.tools))
|
||||
for _, tool := range r.tools {
|
||||
sorted := r.sortedToolNames()
|
||||
definitions := make([]providers.ToolDefinition, 0, len(sorted))
|
||||
for _, name := range sorted {
|
||||
tool := r.tools[name]
|
||||
schema := ToolToSchema(tool)
|
||||
|
||||
// Safely extract nested values with type checks
|
||||
@@ -155,11 +172,7 @@ func (r *ToolRegistry) List() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
names := make([]string, 0, len(r.tools))
|
||||
for name := range r.tools {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
return r.sortedToolNames()
|
||||
}
|
||||
|
||||
// Count returns the number of registered tools.
|
||||
@@ -175,8 +188,10 @@ func (r *ToolRegistry) GetSummaries() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
summaries := make([]string, 0, len(r.tools))
|
||||
for _, tool := range r.tools {
|
||||
sorted := r.sortedToolNames()
|
||||
summaries := make([]string, 0, len(sorted))
|
||||
for _, name := range sorted {
|
||||
tool := r.tools[name]
|
||||
summaries = append(summaries, fmt.Sprintf("- `%s` - %s", tool.Name(), tool.Description()))
|
||||
}
|
||||
return summaries
|
||||
|
||||
+1
-2
@@ -76,10 +76,9 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
||||
func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Config) *ExecTool {
|
||||
denyPatterns := make([]*regexp.Regexp, 0)
|
||||
|
||||
enableDenyPatterns := true
|
||||
if config != nil {
|
||||
execConfig := config.Tools.Exec
|
||||
enableDenyPatterns = execConfig.EnableDenyPatterns
|
||||
enableDenyPatterns := execConfig.EnableDenyPatterns
|
||||
if enableDenyPatterns {
|
||||
denyPatterns = append(denyPatterns, defaultDenyPatterns...)
|
||||
if len(execConfig.CustomDenyPatterns) > 0 {
|
||||
|
||||
+3
-2
@@ -3,6 +3,7 @@ package tools
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SpawnTool struct {
|
||||
@@ -66,8 +67,8 @@ func (t *SpawnTool) SetAllowlistChecker(check func(targetAgentID string) bool) {
|
||||
|
||||
func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
task, ok := args["task"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("task is required")
|
||||
if !ok || strings.TrimSpace(task) == "" {
|
||||
return ErrorResult("task is required and must be a non-empty string")
|
||||
}
|
||||
|
||||
label, _ := args["label"].(string)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSpawnTool_Execute_EmptyTask(t *testing.T) {
|
||||
provider := &MockLLMProvider{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
||||
tool := NewSpawnTool(manager)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args map[string]any
|
||||
}{
|
||||
{"empty string", map[string]any{"task": ""}},
|
||||
{"whitespace only", map[string]any{"task": " "}},
|
||||
{"tabs and newlines", map[string]any{"task": "\t\n "}},
|
||||
{"missing task key", map[string]any{"label": "test"}},
|
||||
{"wrong type", map[string]any{"task": 123}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tool.Execute(ctx, tt.args)
|
||||
if result == nil {
|
||||
t.Fatal("Result should not be nil")
|
||||
}
|
||||
if !result.IsError {
|
||||
t.Error("Expected error for invalid task parameter")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "task is required") {
|
||||
t.Errorf("Error message should mention 'task is required', got: %s", result.ForLLM)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnTool_Execute_ValidTask(t *testing.T) {
|
||||
provider := &MockLLMProvider{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil)
|
||||
tool := NewSpawnTool(manager)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]any{
|
||||
"task": "Write a haiku about coding",
|
||||
"label": "haiku-task",
|
||||
}
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
if result == nil {
|
||||
t.Fatal("Result should not be nil")
|
||||
}
|
||||
if result.IsError {
|
||||
t.Errorf("Expected success for valid task, got error: %s", result.ForLLM)
|
||||
}
|
||||
if !result.Async {
|
||||
t.Error("SpawnTool should return async result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnTool_Execute_NilManager(t *testing.T) {
|
||||
tool := NewSpawnTool(nil)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]any{"task": "test task"}
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
if !result.IsError {
|
||||
t.Error("Expected error for nil manager")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "Subagent manager not configured") {
|
||||
t.Errorf("Error message should mention manager not configured, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
@@ -132,12 +132,12 @@ After completing the task, provide a clear summary of what was done.`
|
||||
},
|
||||
}
|
||||
|
||||
// Check if context is already cancelled before starting
|
||||
// Check if context is already canceled before starting
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
sm.mu.Lock()
|
||||
task.Status = "cancelled"
|
||||
task.Result = "Task cancelled before execution"
|
||||
task.Status = "canceled"
|
||||
task.Result = "Task canceled before execution"
|
||||
sm.mu.Unlock()
|
||||
return
|
||||
default:
|
||||
@@ -185,10 +185,10 @@ After completing the task, provide a clear summary of what was done.`
|
||||
if err != nil {
|
||||
task.Status = "failed"
|
||||
task.Result = fmt.Sprintf("Error: %v", err)
|
||||
// Check if it was cancelled
|
||||
// Check if it was canceled
|
||||
if ctx.Err() != nil {
|
||||
task.Status = "cancelled"
|
||||
task.Result = "Task cancelled during execution"
|
||||
task.Status = "canceled"
|
||||
task.Result = "Task canceled during execution"
|
||||
}
|
||||
result = &ToolResult{
|
||||
ForLLM: task.Result,
|
||||
|
||||
Reference in New Issue
Block a user