feat(fmt): Run formatters

This commit is contained in:
Artem Yadelskyi
2026-02-18 21:48:23 +02:00
parent b1e3b11a5d
commit 9e120f90ea
96 changed files with 1239 additions and 976 deletions
+5 -5
View File
@@ -6,8 +6,8 @@ import "context"
type Tool interface {
Name() string
Description() string
Parameters() map[string]interface{}
Execute(ctx context.Context, args map[string]interface{}) *ToolResult
Parameters() map[string]any
Execute(ctx context.Context, args map[string]any) *ToolResult
}
// ContextualTool is an optional interface that tools can implement
@@ -69,10 +69,10 @@ type AsyncTool interface {
SetCallback(cb AsyncCallback)
}
func ToolToSchema(tool Tool) map[string]interface{} {
return map[string]interface{}{
func ToolToSchema(tool Tool) map[string]any {
return map[string]any{
"type": "function",
"function": map[string]interface{}{
"function": map[string]any{
"name": tool.Name(),
"description": tool.Description(),
"parameters": tool.Parameters(),
+20 -18
View File
@@ -30,7 +30,10 @@ type CronTool struct {
// NewCronTool creates a new CronTool
// execTimeout: 0 means no timeout, >0 sets the timeout duration
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
func NewCronTool(
cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool,
execTimeout time.Duration, config *config.Config,
) *CronTool {
execTool := NewExecToolWithConfig(workspace, restrict, config)
execTool.SetTimeout(execTimeout)
return &CronTool{
@@ -52,40 +55,40 @@ func (t *CronTool) Description() string {
}
// Parameters returns the tool parameters schema
func (t *CronTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *CronTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"add", "list", "remove", "enable", "disable"},
"description": "Action to perform. Use 'add' when user wants to schedule a reminder or task.",
},
"message": map[string]interface{}{
"message": map[string]any{
"type": "string",
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.",
},
"command": map[string]interface{}{
"command": map[string]any{
"type": "string",
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
},
"at_seconds": map[string]interface{}{
"at_seconds": map[string]any{
"type": "integer",
"description": "One-time reminder: seconds from now when to trigger (e.g., 600 for 10 minutes later). Use this for one-time reminders like 'remind me in 10 minutes'.",
},
"every_seconds": map[string]interface{}{
"every_seconds": map[string]any{
"type": "integer",
"description": "Recurring interval in seconds (e.g., 3600 for every hour). Use this ONLY for recurring tasks like 'every 2 hours' or 'daily reminder'.",
},
"cron_expr": map[string]interface{}{
"cron_expr": map[string]any{
"type": "string",
"description": "Cron expression for complex recurring schedules (e.g., '0 9 * * *' for daily at 9am). Use this for complex recurring schedules.",
},
"job_id": map[string]interface{}{
"job_id": map[string]any{
"type": "string",
"description": "Job ID (for remove/enable/disable)",
},
"deliver": map[string]interface{}{
"deliver": map[string]any{
"type": "boolean",
"description": "If true, send message directly to channel. If false, let agent process message (for complex tasks). Default: true",
},
@@ -103,7 +106,7 @@ func (t *CronTool) SetContext(channel, chatID string) {
}
// Execute runs the tool with the given arguments
func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *CronTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
action, ok := args["action"].(string)
if !ok {
return ErrorResult("action is required")
@@ -125,7 +128,7 @@ func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *To
}
}
func (t *CronTool) addJob(args map[string]interface{}) *ToolResult {
func (t *CronTool) addJob(args map[string]any) *ToolResult {
t.mu.RLock()
channel := t.channel
chatID := t.chatID
@@ -233,7 +236,7 @@ func (t *CronTool) listJobs() *ToolResult {
return SilentResult(result)
}
func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
func (t *CronTool) removeJob(args map[string]any) *ToolResult {
jobID, ok := args["job_id"].(string)
if !ok || jobID == "" {
return ErrorResult("job_id is required for remove")
@@ -245,7 +248,7 @@ func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
}
func (t *CronTool) enableJob(args map[string]interface{}, enable bool) *ToolResult {
func (t *CronTool) enableJob(args map[string]any, enable bool) *ToolResult {
jobID, ok := args["job_id"].(string)
if !ok || jobID == "" {
return ErrorResult("job_id is required for enable/disable")
@@ -279,7 +282,7 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
// Execute command if present
if job.Payload.Command != "" {
args := map[string]interface{}{
args := map[string]any{
"command": job.Payload.Command,
}
@@ -320,7 +323,6 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
channel,
chatID,
)
if err != nil {
return fmt.Sprintf("Error: %v", err)
}
+18 -16
View File
@@ -30,19 +30,19 @@ func (t *EditFileTool) Description() string {
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file."
}
func (t *EditFileTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *EditFileTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"properties": map[string]any{
"path": map[string]any{
"type": "string",
"description": "The file path to edit",
},
"old_text": map[string]interface{}{
"old_text": map[string]any{
"type": "string",
"description": "The exact text to find and replace",
},
"new_text": map[string]interface{}{
"new_text": map[string]any{
"type": "string",
"description": "The text to replace with",
},
@@ -51,7 +51,7 @@ func (t *EditFileTool) Parameters() map[string]interface{} {
}
}
func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *EditFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -89,12 +89,14 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{})
count := strings.Count(contentStr, oldText)
if count > 1 {
return ErrorResult(fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count))
return ErrorResult(
fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count),
)
}
newContent := strings.Replace(contentStr, oldText, newText, 1)
if err := os.WriteFile(resolvedPath, []byte(newContent), 0644); err != nil {
if err := os.WriteFile(resolvedPath, []byte(newContent), 0o644); err != nil {
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
}
@@ -118,15 +120,15 @@ func (t *AppendFileTool) Description() string {
return "Append content to the end of a file"
}
func (t *AppendFileTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *AppendFileTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"properties": map[string]any{
"path": map[string]any{
"type": "string",
"description": "The file path to append to",
},
"content": map[string]interface{}{
"content": map[string]any{
"type": "string",
"description": "The content to append",
},
@@ -135,7 +137,7 @@ func (t *AppendFileTool) Parameters() map[string]interface{} {
}
}
func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *AppendFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -151,7 +153,7 @@ func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{
return ErrorResult(err.Error())
}
f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to open file: %v", err))
}
+16 -16
View File
@@ -12,11 +12,11 @@ import (
func TestEditTool_EditFile_Success(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0644)
os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0o644)
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"old_text": "World",
"new_text": "Universe",
@@ -60,7 +60,7 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"old_text": "old",
"new_text": "new",
@@ -83,11 +83,11 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("Hello World"), 0644)
os.WriteFile(testFile, []byte("Hello World"), 0o644)
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"old_text": "Goodbye",
"new_text": "Hello",
@@ -110,11 +110,11 @@ func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
func TestEditTool_EditFile_MultipleMatches(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("test test test"), 0644)
os.WriteFile(testFile, []byte("test test test"), 0o644)
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"old_text": "test",
"new_text": "done",
@@ -138,11 +138,11 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
tmpDir := t.TempDir()
otherDir := t.TempDir()
testFile := filepath.Join(otherDir, "test.txt")
os.WriteFile(testFile, []byte("content"), 0644)
os.WriteFile(testFile, []byte("content"), 0o644)
tool := NewEditFileTool(tmpDir, true) // Restrict to tmpDir
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"old_text": "content",
"new_text": "new",
@@ -165,7 +165,7 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
func TestEditTool_EditFile_MissingPath(t *testing.T) {
tool := NewEditFileTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"old_text": "old",
"new_text": "new",
}
@@ -182,7 +182,7 @@ func TestEditTool_EditFile_MissingPath(t *testing.T) {
func TestEditTool_EditFile_MissingOldText(t *testing.T) {
tool := NewEditFileTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": "/tmp/test.txt",
"new_text": "new",
}
@@ -199,7 +199,7 @@ func TestEditTool_EditFile_MissingOldText(t *testing.T) {
func TestEditTool_EditFile_MissingNewText(t *testing.T) {
tool := NewEditFileTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": "/tmp/test.txt",
"old_text": "old",
}
@@ -216,11 +216,11 @@ func TestEditTool_EditFile_MissingNewText(t *testing.T) {
func TestEditTool_AppendFile_Success(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("Initial content"), 0644)
os.WriteFile(testFile, []byte("Initial content"), 0o644)
tool := NewAppendFileTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"content": "\nAppended content",
}
@@ -260,7 +260,7 @@ func TestEditTool_AppendFile_Success(t *testing.T) {
func TestEditTool_AppendFile_MissingPath(t *testing.T) {
tool := NewAppendFileTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"content": "test",
}
@@ -276,7 +276,7 @@ func TestEditTool_AppendFile_MissingPath(t *testing.T) {
func TestEditTool_AppendFile_MissingContent(t *testing.T) {
tool := NewAppendFileTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": "/tmp/test.txt",
}
+18 -18
View File
@@ -94,11 +94,11 @@ func (t *ReadFileTool) Description() string {
return "Read the contents of a file"
}
func (t *ReadFileTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *ReadFileTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"properties": map[string]any{
"path": map[string]any{
"type": "string",
"description": "Path to the file to read",
},
@@ -107,7 +107,7 @@ func (t *ReadFileTool) Parameters() map[string]interface{} {
}
}
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -143,15 +143,15 @@ func (t *WriteFileTool) Description() string {
return "Write content to a file"
}
func (t *WriteFileTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *WriteFileTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"properties": map[string]any{
"path": map[string]any{
"type": "string",
"description": "Path to the file to write",
},
"content": map[string]interface{}{
"content": map[string]any{
"type": "string",
"description": "Content to write to the file",
},
@@ -160,7 +160,7 @@ func (t *WriteFileTool) Parameters() map[string]interface{} {
}
}
func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -177,11 +177,11 @@ func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}
}
dir := filepath.Dir(resolvedPath)
if err := os.MkdirAll(dir, 0755); err != nil {
if err := os.MkdirAll(dir, 0o755); err != nil {
return ErrorResult(fmt.Sprintf("failed to create directory: %v", err))
}
if err := os.WriteFile(resolvedPath, []byte(content), 0644); err != nil {
if err := os.WriteFile(resolvedPath, []byte(content), 0o644); err != nil {
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
}
@@ -205,11 +205,11 @@ func (t *ListDirTool) Description() string {
return "List files and directories in a path"
}
func (t *ListDirTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *ListDirTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"properties": map[string]any{
"path": map[string]any{
"type": "string",
"description": "Path to list",
},
@@ -218,7 +218,7 @@ func (t *ListDirTool) Parameters() map[string]interface{} {
}
}
func (t *ListDirTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
path = "."
+19 -19
View File
@@ -12,11 +12,11 @@ import (
func TestFilesystemTool_ReadFile_Success(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("test content"), 0644)
os.WriteFile(testFile, []byte("test content"), 0o644)
tool := &ReadFileTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
}
@@ -43,7 +43,7 @@ func TestFilesystemTool_ReadFile_Success(t *testing.T) {
func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
tool := &ReadFileTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": "/nonexistent_file_12345.txt",
}
@@ -64,7 +64,7 @@ func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
func TestFilesystemTool_ReadFile_MissingPath(t *testing.T) {
tool := &ReadFileTool{}
ctx := context.Background()
args := map[string]interface{}{}
args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -86,7 +86,7 @@ func TestFilesystemTool_WriteFile_Success(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"content": "hello world",
}
@@ -125,7 +125,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": testFile,
"content": "test",
}
@@ -151,7 +151,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"content": "test",
}
@@ -167,7 +167,7 @@ func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": "/tmp/test.txt",
}
@@ -179,7 +179,8 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
}
// Should mention required parameter
if !strings.Contains(result.ForLLM, "content is required") && !strings.Contains(result.ForUser, "content is required") {
if !strings.Contains(result.ForLLM, "content is required") &&
!strings.Contains(result.ForUser, "content is required") {
t.Errorf("Expected 'content is required' message, got ForLLM: %s", result.ForLLM)
}
}
@@ -187,13 +188,13 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
// TestFilesystemTool_ListDir_Success verifies successful directory listing
func TestFilesystemTool_ListDir_Success(t *testing.T) {
tmpDir := t.TempDir()
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0644)
os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0644)
os.Mkdir(filepath.Join(tmpDir, "subdir"), 0755)
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0o644)
os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0o644)
os.Mkdir(filepath.Join(tmpDir, "subdir"), 0o755)
tool := &ListDirTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": tmpDir,
}
@@ -217,7 +218,7 @@ func TestFilesystemTool_ListDir_Success(t *testing.T) {
func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
tool := &ListDirTool{}
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"path": "/nonexistent_directory_12345",
}
@@ -238,7 +239,7 @@ func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
tool := &ListDirTool{}
ctx := context.Background()
args := map[string]interface{}{}
args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -250,15 +251,14 @@ func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
// Block paths that look inside workspace but point outside via symlink.
func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
root := t.TempDir()
workspace := filepath.Join(root, "workspace")
if err := os.MkdirAll(workspace, 0755); err != nil {
if err := os.MkdirAll(workspace, 0o755); err != nil {
t.Fatalf("failed to create workspace: %v", err)
}
secret := filepath.Join(root, "secret.txt")
if err := os.WriteFile(secret, []byte("top secret"), 0644); err != nil {
if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil {
t.Fatalf("failed to write secret file: %v", err)
}
@@ -268,7 +268,7 @@ func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
}
tool := NewReadFileTool(workspace, true)
result := tool.Execute(context.Background(), map[string]interface{}{
result := tool.Execute(context.Background(), map[string]any{
"path": link,
})
+17 -15
View File
@@ -24,37 +24,37 @@ func (t *I2CTool) Description() string {
return "Interact with I2C bus devices for reading sensors and controlling peripherals. Actions: detect (list buses), scan (find devices on a bus), read (read bytes from device), write (send bytes to device). Linux only."
}
func (t *I2CTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *I2CTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"detect", "scan", "read", "write"},
"description": "Action to perform: detect (list available I2C buses), scan (find devices on a bus), read (read bytes from a device), write (send bytes to a device)",
},
"bus": map[string]interface{}{
"bus": map[string]any{
"type": "string",
"description": "I2C bus number (e.g. \"1\" for /dev/i2c-1). Required for scan/read/write.",
},
"address": map[string]interface{}{
"address": map[string]any{
"type": "integer",
"description": "7-bit I2C device address (0x03-0x77). Required for read/write.",
},
"register": map[string]interface{}{
"register": map[string]any{
"type": "integer",
"description": "Register address to read from or write to. If set, sends register byte before read/write.",
},
"data": map[string]interface{}{
"data": map[string]any{
"type": "array",
"items": map[string]interface{}{"type": "integer"},
"items": map[string]any{"type": "integer"},
"description": "Bytes to write (0-255 each). Required for write action.",
},
"length": map[string]interface{}{
"length": map[string]any{
"type": "integer",
"description": "Number of bytes to read (1-256). Default: 1. Used with read action.",
},
"confirm": map[string]interface{}{
"confirm": map[string]any{
"type": "boolean",
"description": "Must be true for write operations. Safety guard to prevent accidental writes.",
},
@@ -63,7 +63,7 @@ func (t *I2CTool) Parameters() map[string]interface{} {
}
}
func (t *I2CTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *I2CTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
if runtime.GOOS != "linux" {
return ErrorResult("I2C is only supported on Linux. This tool requires /dev/i2c-* device files.")
}
@@ -95,7 +95,9 @@ func (t *I2CTool) detect() *ToolResult {
}
if len(matches) == 0 {
return SilentResult("No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)")
return SilentResult(
"No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)",
)
}
type busInfo struct {
@@ -122,7 +124,7 @@ func isValidBusID(id string) bool {
}
// parseI2CAddress extracts and validates an I2C address from args
func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
func parseI2CAddress(args map[string]any) (int, *ToolResult) {
addrFloat, ok := args["address"].(float64)
if !ok {
return 0, ErrorResult("address is required (e.g. 0x38 for AHT20)")
@@ -135,7 +137,7 @@ func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
}
// parseI2CBus extracts and validates an I2C bus from args
func parseI2CBus(args map[string]interface{}) (string, *ToolResult) {
func parseI2CBus(args map[string]any) (string, *ToolResult) {
bus, ok := args["bus"].(string)
if !ok || bus == "" {
return "", ErrorResult("bus is required (e.g. \"1\" for /dev/i2c-1)")
+12 -8
View File
@@ -74,7 +74,7 @@ func smbusProbe(fd int, addr int, hasQuick bool) bool {
// scan probes valid 7-bit addresses on a bus for connected devices.
// Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO:
// SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges.
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
func (t *I2CTool) scan(args map[string]any) *ToolResult {
bus, errResult := parseI2CBus(args)
if errResult != nil {
return errResult
@@ -99,7 +99,9 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
hasReadByte := funcs&i2cFuncSmbusReadByte != 0
if !hasQuick && !hasReadByte {
return ErrorResult(fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath))
return ErrorResult(
fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath),
)
}
type deviceEntry struct {
@@ -133,7 +135,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
}
result, _ := json.MarshalIndent(map[string]interface{}{
result, _ := json.MarshalIndent(map[string]any{
"bus": devPath,
"devices": found,
"count": len(found),
@@ -142,7 +144,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
}
// readDevice reads bytes from an I2C device, optionally at a specific register
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
bus, errResult := parseI2CBus(args)
if errResult != nil {
return errResult
@@ -201,7 +203,7 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
intBytes[i] = int(buf[i])
}
result, _ := json.MarshalIndent(map[string]interface{}{
result, _ := json.MarshalIndent(map[string]any{
"bus": devPath,
"address": fmt.Sprintf("0x%02x", addr),
"bytes": intBytes,
@@ -212,10 +214,12 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
}
// writeDevice writes bytes to an I2C device, optionally at a specific register
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
confirm, _ := args["confirm"].(bool)
if !confirm {
return ErrorResult("write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.")
return ErrorResult(
"write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.",
)
}
bus, errResult := parseI2CBus(args)
@@ -228,7 +232,7 @@ func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
return errResult
}
dataRaw, ok := args["data"].([]interface{})
dataRaw, ok := args["data"].([]any)
if !ok || len(dataRaw) == 0 {
return ErrorResult("data is required for write (array of byte values 0-255)")
}
+3 -3
View File
@@ -3,16 +3,16 @@
package tools
// scan is a stub for non-Linux platforms.
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
func (t *I2CTool) scan(args map[string]any) *ToolResult {
return ErrorResult("I2C is only supported on Linux")
}
// readDevice is a stub for non-Linux platforms.
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
return ErrorResult("I2C is only supported on Linux")
}
// writeDevice is a stub for non-Linux platforms.
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
return ErrorResult("I2C is only supported on Linux")
}
+7 -7
View File
@@ -26,19 +26,19 @@ func (t *MessageTool) Description() string {
return "Send a message to user on a chat channel. Use this when you want to communicate something."
}
func (t *MessageTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *MessageTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"content": map[string]interface{}{
"properties": map[string]any{
"content": map[string]any{
"type": "string",
"description": "The message content to send",
},
"channel": map[string]interface{}{
"channel": map[string]any{
"type": "string",
"description": "Optional: target channel (telegram, whatsapp, etc.)",
},
"chat_id": map[string]interface{}{
"chat_id": map[string]any{
"type": "string",
"description": "Optional: target chat/user ID",
},
@@ -62,7 +62,7 @@ func (t *MessageTool) SetSendCallback(callback SendCallback) {
t.sendCallback = callback
}
func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
content, ok := args["content"].(string)
if !ok {
return &ToolResult{ForLLM: "content is required", IsError: true}
+10 -10
View File
@@ -19,7 +19,7 @@ func TestMessageTool_Execute_Success(t *testing.T) {
})
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"content": "Hello, world!",
}
@@ -70,7 +70,7 @@ func TestMessageTool_Execute_WithCustomChannel(t *testing.T) {
})
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"content": "Test message",
"channel": "custom-channel",
"chat_id": "custom-chat-id",
@@ -104,7 +104,7 @@ func TestMessageTool_Execute_SendFailure(t *testing.T) {
})
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"content": "Test message",
}
@@ -136,7 +136,7 @@ func TestMessageTool_Execute_MissingContent(t *testing.T) {
tool.SetContext("test-channel", "test-chat-id")
ctx := context.Background()
args := map[string]interface{}{} // content missing
args := map[string]any{} // content missing
result := tool.Execute(ctx, args)
@@ -158,7 +158,7 @@ func TestMessageTool_Execute_NoTargetChannel(t *testing.T) {
})
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"content": "Test message",
}
@@ -179,7 +179,7 @@ func TestMessageTool_Execute_NotConfigured(t *testing.T) {
// No SetSendCallback called
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"content": "Test message",
}
@@ -219,7 +219,7 @@ func TestMessageTool_Parameters(t *testing.T) {
t.Error("Expected type 'object'")
}
props, ok := params["properties"].(map[string]interface{})
props, ok := params["properties"].(map[string]any)
if !ok {
t.Fatal("Expected properties to be a map")
}
@@ -231,7 +231,7 @@ func TestMessageTool_Parameters(t *testing.T) {
}
// Check content property
contentProp, ok := props["content"].(map[string]interface{})
contentProp, ok := props["content"].(map[string]any)
if !ok {
t.Error("Expected 'content' property")
}
@@ -240,7 +240,7 @@ func TestMessageTool_Parameters(t *testing.T) {
}
// Check channel property (optional)
channelProp, ok := props["channel"].(map[string]interface{})
channelProp, ok := props["channel"].(map[string]any)
if !ok {
t.Error("Expected 'channel' property")
}
@@ -249,7 +249,7 @@ func TestMessageTool_Parameters(t *testing.T) {
}
// Check chat_id property (optional)
chatIDProp, ok := props["chat_id"].(map[string]interface{})
chatIDProp, ok := props["chat_id"].(map[string]any)
if !ok {
t.Error("Expected 'chat_id' property")
}
+18 -12
View File
@@ -34,16 +34,22 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) {
return tool, ok
}
func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]interface{}) *ToolResult {
func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]any) *ToolResult {
return r.ExecuteWithContext(ctx, name, args, "", "", nil)
}
// ExecuteWithContext executes a tool with channel/chatID context and optional async callback.
// If the tool implements AsyncTool and a non-nil callback is provided,
// the callback will be set on the tool before execution.
func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args map[string]interface{}, channel, chatID string, asyncCallback AsyncCallback) *ToolResult {
func (r *ToolRegistry) ExecuteWithContext(
ctx context.Context,
name string,
args map[string]any,
channel, chatID string,
asyncCallback AsyncCallback,
) *ToolResult {
logger.InfoCF("tool", "Tool execution started",
map[string]interface{}{
map[string]any{
"tool": name,
"args": args,
})
@@ -51,7 +57,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
tool, ok := r.Get(name)
if !ok {
logger.ErrorCF("tool", "Tool not found",
map[string]interface{}{
map[string]any{
"tool": name,
})
return ErrorResult(fmt.Sprintf("tool %q not found", name)).WithError(fmt.Errorf("tool not found"))
@@ -66,7 +72,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
if asyncTool, ok := tool.(AsyncTool); ok && asyncCallback != nil {
asyncTool.SetCallback(asyncCallback)
logger.DebugCF("tool", "Async callback injected",
map[string]interface{}{
map[string]any{
"tool": name,
})
}
@@ -78,20 +84,20 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
// Log based on result type
if result.IsError {
logger.ErrorCF("tool", "Tool execution failed",
map[string]interface{}{
map[string]any{
"tool": name,
"duration": duration.Milliseconds(),
"error": result.ForLLM,
})
} else if result.Async {
logger.InfoCF("tool", "Tool started (async)",
map[string]interface{}{
map[string]any{
"tool": name,
"duration": duration.Milliseconds(),
})
} else {
logger.InfoCF("tool", "Tool execution completed",
map[string]interface{}{
map[string]any{
"tool": name,
"duration_ms": duration.Milliseconds(),
"result_length": len(result.ForLLM),
@@ -101,11 +107,11 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
return result
}
func (r *ToolRegistry) GetDefinitions() []map[string]interface{} {
func (r *ToolRegistry) GetDefinitions() []map[string]any {
r.mu.RLock()
defer r.mu.RUnlock()
definitions := make([]map[string]interface{}, 0, len(r.tools))
definitions := make([]map[string]any, 0, len(r.tools))
for _, tool := range r.tools {
definitions = append(definitions, ToolToSchema(tool))
}
@@ -123,14 +129,14 @@ func (r *ToolRegistry) ToProviderDefs() []providers.ToolDefinition {
schema := ToolToSchema(tool)
// Safely extract nested values with type checks
fn, ok := schema["function"].(map[string]interface{})
fn, ok := schema["function"].(map[string]any)
if !ok {
continue
}
name, _ := fn["name"].(string)
desc, _ := fn["description"].(string)
params, _ := fn["parameters"].(map[string]interface{})
params, _ := fn["parameters"].(map[string]any)
definitions = append(definitions, providers.ToolDefinition{
Type: "function",
+1 -1
View File
@@ -192,7 +192,7 @@ func TestToolResultJSONStructure(t *testing.T) {
}
// Verify JSON structure
var parsed map[string]interface{}
var parsed map[string]any
if err := json.Unmarshal(data, &parsed); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
+6 -6
View File
@@ -118,15 +118,15 @@ func (t *ExecTool) Description() string {
return "Execute a shell command and return its output. Use with caution."
}
func (t *ExecTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *ExecTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"command": map[string]interface{}{
"properties": map[string]any{
"command": map[string]any{
"type": "string",
"description": "The shell command to execute",
},
"working_dir": map[string]interface{}{
"working_dir": map[string]any{
"type": "string",
"description": "Optional working directory for the command",
},
@@ -135,7 +135,7 @@ func (t *ExecTool) Parameters() map[string]interface{} {
}
}
func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
command, ok := args["command"].(string)
if !ok {
return ErrorResult("command is required")
+15 -11
View File
@@ -14,7 +14,7 @@ func TestShellTool_Success(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"command": "echo 'hello world'",
}
@@ -41,7 +41,7 @@ func TestShellTool_Failure(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"command": "ls /nonexistent_directory_12345",
}
@@ -69,7 +69,7 @@ func TestShellTool_Timeout(t *testing.T) {
tool.SetTimeout(100 * time.Millisecond)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"command": "sleep 10",
}
@@ -91,12 +91,12 @@ func TestShellTool_WorkingDir(t *testing.T) {
// Create temp directory
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("test content"), 0644)
os.WriteFile(testFile, []byte("test content"), 0o644)
tool := NewExecTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"command": "cat test.txt",
"working_dir": tmpDir,
}
@@ -117,7 +117,7 @@ func TestShellTool_DangerousCommand(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"command": "rm -rf /",
}
@@ -138,7 +138,7 @@ func TestShellTool_MissingCommand(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
args := map[string]interface{}{}
args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -153,7 +153,7 @@ func TestShellTool_StderrCapture(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"command": "sh -c 'echo stdout; echo stderr >&2'",
}
@@ -174,7 +174,7 @@ func TestShellTool_OutputTruncation(t *testing.T) {
ctx := context.Background()
// Generate long output (>10000 chars)
args := map[string]interface{}{
args := map[string]any{
"command": "python3 -c \"print('x' * 20000)\" || echo " + strings.Repeat("x", 20000),
}
@@ -193,7 +193,7 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
tool.SetRestrictToWorkspace(true)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"command": "cat ../../etc/passwd",
}
@@ -205,6 +205,10 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
}
if !strings.Contains(result.ForLLM, "blocked") && !strings.Contains(result.ForUser, "blocked") {
t.Errorf("Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s", result.ForLLM, result.ForUser)
t.Errorf(
"Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s",
result.ForLLM,
result.ForUser,
)
}
}
+7 -7
View File
@@ -34,19 +34,19 @@ func (t *SpawnTool) Description() string {
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done."
}
func (t *SpawnTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *SpawnTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"task": map[string]interface{}{
"properties": map[string]any{
"task": map[string]any{
"type": "string",
"description": "The task for subagent to complete",
},
"label": map[string]interface{}{
"label": map[string]any{
"type": "string",
"description": "Optional short label for the task (for display)",
},
"agent_id": map[string]interface{}{
"agent_id": map[string]any{
"type": "string",
"description": "Optional target agent ID to delegate the task to",
},
@@ -64,7 +64,7 @@ func (t *SpawnTool) SetAllowlistChecker(check func(targetAgentID string) bool) {
t.allowlistCheck = check
}
func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
task, ok := args["task"].(string)
if !ok {
return ErrorResult("task is required")
+17 -15
View File
@@ -24,41 +24,41 @@ func (t *SPITool) Description() string {
return "Interact with SPI bus devices for high-speed peripheral communication. Actions: list (find SPI devices), transfer (full-duplex send/receive), read (receive bytes). Linux only."
}
func (t *SPITool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *SPITool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"list", "transfer", "read"},
"description": "Action to perform: list (find available SPI devices), transfer (full-duplex send/receive), read (receive bytes by sending zeros)",
},
"device": map[string]interface{}{
"device": map[string]any{
"type": "string",
"description": "SPI device identifier (e.g. \"2.0\" for /dev/spidev2.0). Required for transfer/read.",
},
"speed": map[string]interface{}{
"speed": map[string]any{
"type": "integer",
"description": "SPI clock speed in Hz. Default: 1000000 (1 MHz).",
},
"mode": map[string]interface{}{
"mode": map[string]any{
"type": "integer",
"description": "SPI mode (0-3). Default: 0. Mode sets CPOL and CPHA: 0=0,0 1=0,1 2=1,0 3=1,1.",
},
"bits": map[string]interface{}{
"bits": map[string]any{
"type": "integer",
"description": "Bits per word. Default: 8.",
},
"data": map[string]interface{}{
"data": map[string]any{
"type": "array",
"items": map[string]interface{}{"type": "integer"},
"items": map[string]any{"type": "integer"},
"description": "Bytes to send (0-255 each). Required for transfer action.",
},
"length": map[string]interface{}{
"length": map[string]any{
"type": "integer",
"description": "Number of bytes to read (1-4096). Required for read action.",
},
"confirm": map[string]interface{}{
"confirm": map[string]any{
"type": "boolean",
"description": "Must be true for transfer operations. Safety guard to prevent accidental writes.",
},
@@ -67,7 +67,7 @@ func (t *SPITool) Parameters() map[string]interface{} {
}
}
func (t *SPITool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *SPITool) Execute(ctx context.Context, args map[string]any) *ToolResult {
if runtime.GOOS != "linux" {
return ErrorResult("SPI is only supported on Linux. This tool requires /dev/spidev* device files.")
}
@@ -97,7 +97,9 @@ func (t *SPITool) list() *ToolResult {
}
if len(matches) == 0 {
return SilentResult("No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded")
return SilentResult(
"No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded",
)
}
type devInfo struct {
@@ -118,7 +120,7 @@ func (t *SPITool) list() *ToolResult {
}
// parseSPIArgs extracts and validates common SPI parameters
func parseSPIArgs(args map[string]interface{}) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
dev, ok := args["device"].(string)
if !ok || dev == "" {
return "", 0, 0, 0, "device is required (e.g. \"2.0\" for /dev/spidev2.0)"
+8 -6
View File
@@ -66,10 +66,12 @@ func configureSPI(devPath string, mode uint8, bits uint8, speed uint32) (int, *T
}
// transfer performs a full-duplex SPI transfer
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
func (t *SPITool) transfer(args map[string]any) *ToolResult {
confirm, _ := args["confirm"].(bool)
if !confirm {
return ErrorResult("transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.")
return ErrorResult(
"transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.",
)
}
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
@@ -77,7 +79,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
return ErrorResult(errMsg)
}
dataRaw, ok := args["data"].([]interface{})
dataRaw, ok := args["data"].([]any)
if !ok || len(dataRaw) == 0 {
return ErrorResult("data is required for transfer (array of byte values 0-255)")
}
@@ -130,7 +132,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
intBytes[i] = int(b)
}
result, _ := json.MarshalIndent(map[string]interface{}{
result, _ := json.MarshalIndent(map[string]any{
"device": devPath,
"sent": len(txBuf),
"received": intBytes,
@@ -140,7 +142,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
}
// readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed)
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
func (t *SPITool) readDevice(args map[string]any) *ToolResult {
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
if errMsg != "" {
return ErrorResult(errMsg)
@@ -186,7 +188,7 @@ func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
intBytes[i] = int(b)
}
result, _ := json.MarshalIndent(map[string]interface{}{
result, _ := json.MarshalIndent(map[string]any{
"device": devPath,
"bytes": intBytes,
"hex": hexBytes,
+2 -2
View File
@@ -3,11 +3,11 @@
package tools
// transfer is a stub for non-Linux platforms.
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
func (t *SPITool) transfer(args map[string]any) *ToolResult {
return ErrorResult("SPI is only supported on Linux")
}
// readDevice is a stub for non-Linux platforms.
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
func (t *SPITool) readDevice(args map[string]any) *ToolResult {
return ErrorResult("SPI is only supported on Linux")
}
+22 -10
View File
@@ -34,7 +34,11 @@ type SubagentManager struct {
nextID int
}
func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager {
func NewSubagentManager(
provider providers.LLMProvider,
defaultModel, workspace string,
bus *bus.MessageBus,
) *SubagentManager {
return &SubagentManager{
tasks: make(map[string]*SubagentTask),
provider: provider,
@@ -62,7 +66,11 @@ func (sm *SubagentManager) RegisterTool(tool Tool) {
sm.tools.Register(tool)
}
func (sm *SubagentManager) Spawn(ctx context.Context, task, label, agentID, originChannel, originChatID string, callback AsyncCallback) (string, error) {
func (sm *SubagentManager) Spawn(
ctx context.Context,
task, label, agentID, originChannel, originChatID string,
callback AsyncCallback,
) (string, error) {
sm.mu.Lock()
defer sm.mu.Unlock()
@@ -168,7 +176,12 @@ After completing the task, provide a clear summary of what was done.`
task.Status = "completed"
task.Result = loopResult.Content
result = &ToolResult{
ForLLM: fmt.Sprintf("Subagent '%s' completed (iterations: %d): %s", task.Label, loopResult.Iterations, loopResult.Content),
ForLLM: fmt.Sprintf(
"Subagent '%s' completed (iterations: %d): %s",
task.Label,
loopResult.Iterations,
loopResult.Content,
),
ForUser: loopResult.Content,
Silent: false,
IsError: false,
@@ -232,15 +245,15 @@ func (t *SubagentTool) Description() string {
return "Execute a subagent task synchronously and return the result. Use this for delegating specific tasks to an independent agent instance. Returns execution summary to user and full details to LLM."
}
func (t *SubagentTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *SubagentTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"task": map[string]interface{}{
"properties": map[string]any{
"task": map[string]any{
"type": "string",
"description": "The task for subagent to complete",
},
"label": map[string]interface{}{
"label": map[string]any{
"type": "string",
"description": "Optional short label for the task (for display)",
},
@@ -254,7 +267,7 @@ func (t *SubagentTool) SetContext(channel, chatID string) {
t.originChatID = chatID
}
func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
task, ok := args["task"].(string)
if !ok {
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
@@ -295,7 +308,6 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{})
"temperature": 0.7,
},
}, messages, t.originChannel, t.originChatID)
if err != nil {
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
}
+16 -10
View File
@@ -12,7 +12,13 @@ import (
// MockLLMProvider is a test implementation of LLMProvider
type MockLLMProvider struct{}
func (m *MockLLMProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
func (m *MockLLMProvider) Chat(
ctx context.Context,
messages []providers.Message,
tools []providers.ToolDefinition,
model string,
options map[string]any,
) (*providers.LLMResponse, error) {
// Find the last user message to generate a response
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == "user" {
@@ -79,13 +85,13 @@ func TestSubagentTool_Parameters(t *testing.T) {
}
// Check properties
props, ok := params["properties"].(map[string]interface{})
props, ok := params["properties"].(map[string]any)
if !ok {
t.Fatal("Properties should be a map")
}
// Verify task parameter
task, ok := props["task"].(map[string]interface{})
task, ok := props["task"].(map[string]any)
if !ok {
t.Fatal("Task parameter should exist")
}
@@ -94,7 +100,7 @@ func TestSubagentTool_Parameters(t *testing.T) {
}
// Verify label parameter
label, ok := props["label"].(map[string]interface{})
label, ok := props["label"].(map[string]any)
if !ok {
t.Fatal("Label parameter should exist")
}
@@ -134,7 +140,7 @@ func TestSubagentTool_Execute_Success(t *testing.T) {
tool.SetContext("telegram", "chat-123")
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"task": "Write a haiku about coding",
"label": "haiku-task",
}
@@ -189,7 +195,7 @@ func TestSubagentTool_Execute_NoLabel(t *testing.T) {
tool := NewSubagentTool(manager)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"task": "Test task without label",
}
@@ -212,7 +218,7 @@ func TestSubagentTool_Execute_MissingTask(t *testing.T) {
tool := NewSubagentTool(manager)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"label": "test",
}
@@ -239,7 +245,7 @@ func TestSubagentTool_Execute_NilManager(t *testing.T) {
tool := NewSubagentTool(nil)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"task": "test task",
}
@@ -268,7 +274,7 @@ func TestSubagentTool_Execute_ContextPassing(t *testing.T) {
tool.SetContext(channel, chatID)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"task": "Test context passing",
}
@@ -295,7 +301,7 @@ func TestSubagentTool_ForUserTruncation(t *testing.T) {
// Create a task that will generate long response
longTask := strings.Repeat("This is a very long task description. ", 100)
args := map[string]interface{}{
args := map[string]any{
"task": longTask,
"label": "long-test",
}
+6 -1
View File
@@ -33,7 +33,12 @@ type ToolLoopResult struct {
// RunToolLoop executes the LLM + tool call iteration loop.
// This is the core agent logic that can be reused by both main agent and subagents.
func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []providers.Message, channel, chatID string) (*ToolLoopResult, error) {
func RunToolLoop(
ctx context.Context,
config ToolLoopConfig,
messages []providers.Message,
channel, chatID string,
) (*ToolLoopResult, error) {
iteration := 0
var finalContent string
+15 -9
View File
@@ -10,11 +10,11 @@ type Message struct {
}
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function *FunctionCall `json:"function,omitempty"`
Name string `json:"name,omitempty"`
Arguments map[string]interface{} `json:"arguments,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Function *FunctionCall `json:"function,omitempty"`
Name string `json:"name,omitempty"`
Arguments map[string]any `json:"arguments,omitempty"`
}
type FunctionCall struct {
@@ -36,7 +36,13 @@ type UsageInfo struct {
}
type LLMProvider interface {
Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error)
Chat(
ctx context.Context,
messages []Message,
tools []ToolDefinition,
model string,
options map[string]any,
) (*LLMResponse, error)
GetDefaultModel() string
}
@@ -46,7 +52,7 @@ type ToolDefinition struct {
}
type ToolFunctionDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]any `json:"parameters"`
}
+30 -18
View File
@@ -183,11 +183,17 @@ type PerplexitySearchProvider struct {
func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
searchURL := "https://api.perplexity.ai/chat/completions"
payload := map[string]interface{}{
payload := map[string]any{
"model": "sonar",
"messages": []map[string]string{
{"role": "system", "content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary."},
{"role": "user", "content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count)},
{
"role": "system",
"content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary.",
},
{
"role": "user",
"content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count),
},
},
"max_tokens": 1000,
}
@@ -295,15 +301,15 @@ func (t *WebSearchTool) Description() string {
return "Search the web for current information. Returns titles, URLs, and snippets from search results."
}
func (t *WebSearchTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *WebSearchTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "Search query",
},
"count": map[string]interface{}{
"count": map[string]any{
"type": "integer",
"description": "Number of results (1-10)",
"minimum": 1.0,
@@ -314,7 +320,7 @@ func (t *WebSearchTool) Parameters() map[string]interface{} {
}
}
func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *WebSearchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
query, ok := args["query"].(string)
if !ok {
return ErrorResult("query is required")
@@ -359,15 +365,15 @@ func (t *WebFetchTool) Description() string {
return "Fetch a URL and extract readable content (HTML to text). Use this to get weather info, news, articles, or any web content."
}
func (t *WebFetchTool) Parameters() map[string]interface{} {
return map[string]interface{}{
func (t *WebFetchTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]interface{}{
"url": map[string]interface{}{
"properties": map[string]any{
"url": map[string]any{
"type": "string",
"description": "URL to fetch",
},
"maxChars": map[string]interface{}{
"maxChars": map[string]any{
"type": "integer",
"description": "Maximum characters to extract",
"minimum": 100.0,
@@ -377,7 +383,7 @@ func (t *WebFetchTool) Parameters() map[string]interface{} {
}
}
func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
urlStr, ok := args["url"].(string)
if !ok {
return ErrorResult("url is required")
@@ -442,7 +448,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
var text, extractor string
if strings.Contains(contentType, "application/json") {
var jsonData interface{}
var jsonData any
if err := json.Unmarshal(body, &jsonData); err == nil {
formatted, _ := json.MarshalIndent(jsonData, "", " ")
text = string(formatted)
@@ -465,7 +471,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
text = text[:maxChars]
}
result := map[string]interface{}{
result := map[string]any{
"url": urlStr,
"status": resp.StatusCode,
"extractor": extractor,
@@ -477,7 +483,13 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
resultJSON, _ := json.MarshalIndent(result, "", " ")
return &ToolResult{
ForLLM: fmt.Sprintf("Fetched %d bytes from %s (extractor: %s, truncated: %v)", len(text), urlStr, extractor, truncated),
ForLLM: fmt.Sprintf(
"Fetched %d bytes from %s (extractor: %s, truncated: %v)",
len(text),
urlStr,
extractor,
truncated,
),
ForUser: string(resultJSON),
}
}
+15 -11
View File
@@ -20,7 +20,7 @@ func TestWebTool_WebFetch_Success(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"url": server.URL,
}
@@ -56,7 +56,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"url": server.URL,
}
@@ -77,7 +77,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"url": "not-a-valid-url",
}
@@ -98,7 +98,7 @@ func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"url": "ftp://example.com/file.txt",
}
@@ -119,7 +119,7 @@ func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
func TestWebTool_WebFetch_MissingURL(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
args := map[string]interface{}{}
args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -147,7 +147,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
tool := NewWebFetchTool(1000) // Limit to 1000 chars
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"url": server.URL,
}
@@ -159,7 +159,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
}
// ForUser should contain truncated content (not the full 20000 chars)
resultMap := make(map[string]interface{})
resultMap := make(map[string]any)
json.Unmarshal([]byte(result.ForUser), &resultMap)
if text, ok := resultMap["text"].(string); ok {
if len(text) > 1100 { // Allow some margin
@@ -191,7 +191,7 @@ func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
func TestWebTool_WebSearch_MissingQuery(t *testing.T) {
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5})
ctx := context.Background()
args := map[string]interface{}{}
args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -206,13 +206,17 @@ func TestWebTool_WebFetch_HTMLExtraction(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<html><body><script>alert('test');</script><style>body{color:red;}</style><h1>Title</h1><p>Content</p></body></html>`))
w.Write(
[]byte(
`<html><body><script>alert('test');</script><style>body{color:red;}</style><h1>Title</h1><p>Content</p></body></html>`,
),
)
}))
defer server.Close()
tool := NewWebFetchTool(50000)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"url": server.URL,
}
@@ -238,7 +242,7 @@ func TestWebTool_WebFetch_HTMLExtraction(t *testing.T) {
func TestWebTool_WebFetch_MissingDomain(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
args := map[string]interface{}{
args := map[string]any{
"url": "https://",
}