merge: resolve conflicts with main

This commit is contained in:
mosir
2026-02-25 21:53:04 +08:00
96 changed files with 3325 additions and 956 deletions
+27 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+79
View File
@@ -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)
}
}
+6 -6
View File
@@ -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,