refactor: mover tools a subpackages individuales

Cada tool ahora vive en su propio subpackage dentro de tools/ (clock, file,
http, knowledgetools, matrix, memorytools, ssh, weather) en lugar de archivos
planos en el paquete raíz tools/. Esto mejora la organización, permite imports
selectivos y reduce acoplamiento entre tools. El paquete tools/ raíz conserva
los tipos base (Def, Param, Result, ToolFunc, Tool, Registry).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 17:16:45 +00:00
parent 8e089ec07e
commit 8d89a762fb
10 changed files with 206 additions and 201 deletions
+10 -8
View File
@@ -1,31 +1,33 @@
package tools package clock
import ( import (
"context" "context"
"fmt" "fmt"
"time" "time"
"github.com/enmanuel/agents/tools"
) )
// NewCurrentTime creates a current_time tool that returns the current date and time. // NewCurrentTime creates a current_time tool that returns the current date and time.
// Useful for agents that need temporal awareness. // Useful for agents that need temporal awareness.
func NewCurrentTime() Tool { func NewCurrentTime() tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "current_time", Name: "current_time",
Description: "Returns the current date and time in the server's timezone. Use this when you need to know the current time or date.", Description: "Returns the current date and time in the server's timezone. Use this when you need to know the current time or date.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "format", Type: "string", Description: "Optional Go time format string. Defaults to RFC3339 if empty.", Required: false}, {Name: "format", Type: "string", Description: "Optional Go time format string. Defaults to RFC3339 if empty.", Required: false},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
layout := getString(args, "format") layout := tools.GetString(args, "format")
if layout == "" { if layout == "" {
layout = time.RFC3339 layout = time.RFC3339
} }
now := time.Now() now := time.Now()
output := fmt.Sprintf("Current time: %s\nTimezone: %s", now.Format(layout), now.Location().String()) output := fmt.Sprintf("Current time: %s\nTimezone: %s", now.Format(layout), now.Location().String())
return Result{Output: output} return tools.Result{Output: output}
}, },
} }
} }
+13 -12
View File
@@ -1,4 +1,4 @@
package tools package file
import ( import (
"context" "context"
@@ -8,37 +8,38 @@ import (
"strings" "strings"
"github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/tools"
) )
// NewReadFile creates a read_file tool that reads local files. // NewReadFile creates a read_file tool that reads local files.
// Validates paths against cfg.AllowedPaths when non-empty. // Validates paths against cfg.AllowedPaths when non-empty.
func NewReadFile(cfg config.FileOpsCfg) Tool { func NewReadFile(cfg config.FileOpsCfg) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "read_file", Name: "read_file",
Description: "Read the contents of a local file.", Description: "Read the contents of a local file.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "path", Type: "string", Description: "Absolute path to the file to read", Required: true}, {Name: "path", Type: "string", Description: "Absolute path to the file to read", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
path := getString(args, "path") path := tools.GetString(args, "path")
if path == "" { if path == "" {
return Result{Err: fmt.Errorf("read_file: path is required")} return tools.Result{Err: fmt.Errorf("read_file: path is required")}
} }
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("read_file: %w", err)} return tools.Result{Err: fmt.Errorf("read_file: %w", err)}
} }
if err := validatePath(absPath, cfg.AllowedPaths); err != nil { if err := validatePath(absPath, cfg.AllowedPaths); err != nil {
return Result{Err: err} return tools.Result{Err: err}
} }
data, err := os.ReadFile(absPath) data, err := os.ReadFile(absPath)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("read_file: %w", err)} return tools.Result{Err: fmt.Errorf("read_file: %w", err)}
} }
// Limit output to 64 KB // Limit output to 64 KB
@@ -47,7 +48,7 @@ func NewReadFile(cfg config.FileOpsCfg) Tool {
content = content[:64*1024] + "\n... (truncated)" content = content[:64*1024] + "\n... (truncated)"
} }
return Result{Output: content} return tools.Result{Output: content}
}, },
} }
} }
+28 -27
View File
@@ -1,4 +1,4 @@
package tools package http
import ( import (
"context" "context"
@@ -10,104 +10,105 @@ import (
"time" "time"
"github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/tools"
) )
// NewHTTPGet creates an http_get tool that performs GET requests. // NewHTTPGet creates an http_get tool that performs GET requests.
// Validates URLs against cfg.AllowedDomains when non-empty. // Validates URLs against cfg.AllowedDomains when non-empty.
func NewHTTPGet(cfg config.HTTPToolCfg) Tool { func NewHTTPGet(cfg config.HTTPToolCfg) tools.Tool {
timeout := cfg.Timeout timeout := cfg.Timeout
if timeout == 0 { if timeout == 0 {
timeout = 30 * time.Second timeout = 30 * time.Second
} }
client := &http.Client{Timeout: timeout} client := &http.Client{Timeout: timeout}
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "http_get", Name: "http_get",
Description: "Perform an HTTP GET request to a URL and return the response body.", Description: "Perform an HTTP GET request to a URL and return the response body.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "url", Type: "string", Description: "The URL to request", Required: true}, {Name: "url", Type: "string", Description: "The URL to request", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
rawURL := getString(args, "url") rawURL := tools.GetString(args, "url")
if rawURL == "" { if rawURL == "" {
return Result{Err: fmt.Errorf("http_get: url is required")} return tools.Result{Err: fmt.Errorf("http_get: url is required")}
} }
if err := validateDomain(rawURL, cfg.AllowedDomains); err != nil { if err := validateDomain(rawURL, cfg.AllowedDomains); err != nil {
return Result{Err: err} return tools.Result{Err: err}
} }
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("http_get: %w", err)} return tools.Result{Err: fmt.Errorf("http_get: %w", err)}
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("http_get: %w", err)} return tools.Result{Err: fmt.Errorf("http_get: %w", err)}
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64 KB limit body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64 KB limit
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("http_get read body: %w", err)} return tools.Result{Err: fmt.Errorf("http_get read body: %w", err)}
} }
return Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)} return tools.Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)}
}, },
} }
} }
// NewHTTPPost creates an http_post tool that performs POST requests with a JSON body. // NewHTTPPost creates an http_post tool that performs POST requests with a JSON body.
// Validates URLs against cfg.AllowedDomains when non-empty. // Validates URLs against cfg.AllowedDomains when non-empty.
func NewHTTPPost(cfg config.HTTPToolCfg) Tool { func NewHTTPPost(cfg config.HTTPToolCfg) tools.Tool {
timeout := cfg.Timeout timeout := cfg.Timeout
if timeout == 0 { if timeout == 0 {
timeout = 30 * time.Second timeout = 30 * time.Second
} }
client := &http.Client{Timeout: timeout} client := &http.Client{Timeout: timeout}
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "http_post", Name: "http_post",
Description: "Perform an HTTP POST request with a JSON body and return the response.", Description: "Perform an HTTP POST request with a JSON body and return the response.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "url", Type: "string", Description: "The URL to request", Required: true}, {Name: "url", Type: "string", Description: "The URL to request", Required: true},
{Name: "body", Type: "string", Description: "The JSON body to send", Required: true}, {Name: "body", Type: "string", Description: "The JSON body to send", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
rawURL := getString(args, "url") rawURL := tools.GetString(args, "url")
if rawURL == "" { if rawURL == "" {
return Result{Err: fmt.Errorf("http_post: url is required")} return tools.Result{Err: fmt.Errorf("http_post: url is required")}
} }
bodyStr := getString(args, "body") bodyStr := tools.GetString(args, "body")
if bodyStr == "" { if bodyStr == "" {
return Result{Err: fmt.Errorf("http_post: body is required")} return tools.Result{Err: fmt.Errorf("http_post: body is required")}
} }
if err := validateDomain(rawURL, cfg.AllowedDomains); err != nil { if err := validateDomain(rawURL, cfg.AllowedDomains); err != nil {
return Result{Err: err} return tools.Result{Err: err}
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, strings.NewReader(bodyStr)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, strings.NewReader(bodyStr))
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("http_post: %w", err)} return tools.Result{Err: fmt.Errorf("http_post: %w", err)}
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("http_post: %w", err)} return tools.Result{Err: fmt.Errorf("http_post: %w", err)}
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("http_post read body: %w", err)} return tools.Result{Err: fmt.Errorf("http_post read body: %w", err)}
} }
return Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)} return tools.Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)}
}, },
} }
} }
@@ -1,4 +1,4 @@
package tools package knowledgetools
import ( import (
"context" "context"
@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/enmanuel/agents/pkg/knowledge" "github.com/enmanuel/agents/pkg/knowledge"
"github.com/enmanuel/agents/tools"
) )
// KnowledgeStore is the subset of knowledge.Store needed by knowledge tools. // KnowledgeStore is the subset of knowledge.Store needed by knowledge tools.
@@ -17,84 +18,84 @@ type KnowledgeStore interface {
} }
// NewKnowledgeSearch creates a tool that searches the knowledge base. // NewKnowledgeSearch creates a tool that searches the knowledge base.
func NewKnowledgeSearch(store KnowledgeStore) Tool { func NewKnowledgeSearch(store KnowledgeStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "knowledge_search", Name: "knowledge_search",
Description: "Search your knowledge base for relevant documents. Returns matching snippets ranked by relevance.", Description: "Search your knowledge base for relevant documents. Returns matching snippets ranked by relevance.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "query", Type: "string", Description: "Search terms or phrase", Required: true}, {Name: "query", Type: "string", Description: "Search terms or phrase", Required: true},
{Name: "limit", Type: "integer", Description: "Max results (default 5)", Required: false}, {Name: "limit", Type: "integer", Description: "Max results (default 5)", Required: false},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
query := getString(args, "query") query := tools.GetString(args, "query")
if query == "" { if query == "" {
return Result{Err: fmt.Errorf("knowledge_search: query is required")} return tools.Result{Err: fmt.Errorf("knowledge_search: query is required")}
} }
limit := getInt(args, "limit") limit := tools.GetInt(args, "limit")
if limit <= 0 { if limit <= 0 {
limit = 5 limit = 5
} }
results, err := store.Search(ctx, query, limit) results, err := store.Search(ctx, query, limit)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("knowledge_search: %w", err)} return tools.Result{Err: fmt.Errorf("knowledge_search: %w", err)}
} }
if len(results) == 0 { if len(results) == 0 {
return Result{Output: "no documents found matching your query"} return tools.Result{Output: "no documents found matching your query"}
} }
var sb strings.Builder var sb strings.Builder
for i, r := range results { for i, r := range results {
fmt.Fprintf(&sb, "%d. **%s** (`%s`)\n %s\n", i+1, r.Title, r.Slug, r.Snippet) fmt.Fprintf(&sb, "%d. **%s** (`%s`)\n %s\n", i+1, r.Title, r.Slug, r.Snippet)
} }
return Result{Output: sb.String()} return tools.Result{Output: sb.String()}
}, },
} }
} }
// NewKnowledgeRead creates a tool that reads a knowledge document. // NewKnowledgeRead creates a tool that reads a knowledge document.
func NewKnowledgeRead(store KnowledgeStore) Tool { func NewKnowledgeRead(store KnowledgeStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "knowledge_read", Name: "knowledge_read",
Description: "Read the full content of a knowledge document by its slug.", Description: "Read the full content of a knowledge document by its slug.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "slug", Type: "string", Description: "Document slug (e.g. \"go-patterns\")", Required: true}, {Name: "slug", Type: "string", Description: "Document slug (e.g. \"go-patterns\")", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
slug := getString(args, "slug") slug := tools.GetString(args, "slug")
if slug == "" { if slug == "" {
return Result{Err: fmt.Errorf("knowledge_read: slug is required")} return tools.Result{Err: fmt.Errorf("knowledge_read: slug is required")}
} }
doc, err := store.Get(ctx, slug) doc, err := store.Get(ctx, slug)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("knowledge_read: %w", err)} return tools.Result{Err: fmt.Errorf("knowledge_read: %w", err)}
} }
return Result{Output: doc.Content} return tools.Result{Output: doc.Content}
}, },
} }
} }
// NewKnowledgeWrite creates a tool that writes a knowledge document. // NewKnowledgeWrite creates a tool that writes a knowledge document.
func NewKnowledgeWrite(store KnowledgeStore) Tool { func NewKnowledgeWrite(store KnowledgeStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "knowledge_write", Name: "knowledge_write",
Description: "Create or update a knowledge document. Use this to save new knowledge or improve existing documents.", Description: "Create or update a knowledge document. Use this to save new knowledge or improve existing documents.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "slug", Type: "string", Description: "Document slug (lowercase, hyphens, e.g. \"matrix-tips\")", Required: true}, {Name: "slug", Type: "string", Description: "Document slug (lowercase, hyphens, e.g. \"matrix-tips\")", Required: true},
{Name: "content", Type: "string", Description: "Full markdown content of the document", Required: true}, {Name: "content", Type: "string", Description: "Full markdown content of the document", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
slug := getString(args, "slug") slug := tools.GetString(args, "slug")
content := getString(args, "content") content := tools.GetString(args, "content")
if slug == "" || content == "" { if slug == "" || content == "" {
return Result{Err: fmt.Errorf("knowledge_write: slug and content are required")} return tools.Result{Err: fmt.Errorf("knowledge_write: slug and content are required")}
} }
err := store.Put(ctx, knowledge.Document{ err := store.Put(ctx, knowledge.Document{
@@ -102,28 +103,28 @@ func NewKnowledgeWrite(store KnowledgeStore) Tool {
Content: content, Content: content,
}) })
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("knowledge_write: %w", err)} return tools.Result{Err: fmt.Errorf("knowledge_write: %w", err)}
} }
return Result{Output: fmt.Sprintf("document saved: %s (%d bytes)", slug, len(content))} return tools.Result{Output: fmt.Sprintf("document saved: %s (%d bytes)", slug, len(content))}
}, },
} }
} }
// NewKnowledgeList creates a tool that lists all knowledge documents. // NewKnowledgeList creates a tool that lists all knowledge documents.
func NewKnowledgeList(store KnowledgeStore) Tool { func NewKnowledgeList(store KnowledgeStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "knowledge_list", Name: "knowledge_list",
Description: "List all documents in your knowledge base with their titles.", Description: "List all documents in your knowledge base with their titles.",
Parameters: []Param{}, Parameters: []tools.Param{},
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
docs, err := store.List(ctx) docs, err := store.List(ctx)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("knowledge_list: %w", err)} return tools.Result{Err: fmt.Errorf("knowledge_list: %w", err)}
} }
if len(docs) == 0 { if len(docs) == 0 {
return Result{Output: "knowledge base is empty"} return tools.Result{Output: "knowledge base is empty"}
} }
var sb strings.Builder var sb strings.Builder
@@ -131,23 +132,7 @@ func NewKnowledgeList(store KnowledgeStore) Tool {
fmt.Fprintf(&sb, "- `%s`: %s (updated %s)\n", fmt.Fprintf(&sb, "- `%s`: %s (updated %s)\n",
d.Slug, d.Title, d.UpdatedAt.Format("2006-01-02")) d.Slug, d.Title, d.UpdatedAt.Format("2006-01-02"))
} }
return Result{Output: sb.String()} return tools.Result{Output: sb.String()}
}, },
} }
} }
// getInt extracts an integer argument by name, returning 0 if missing or wrong type.
func getInt(args map[string]any, key string) int {
v, ok := args[key]
if !ok {
return 0
}
switch n := v.(type) {
case float64:
return int(n)
case int:
return n
default:
return 0
}
}
@@ -1,10 +1,11 @@
package tools package knowledgetools
import ( import (
"context" "context"
"testing" "testing"
"github.com/enmanuel/agents/pkg/knowledge" "github.com/enmanuel/agents/pkg/knowledge"
"github.com/enmanuel/agents/tools"
) )
// mockKnowledgeStore implements KnowledgeStore for testing. // mockKnowledgeStore implements KnowledgeStore for testing.
@@ -173,16 +174,9 @@ func TestGetInt(t *testing.T) {
{map[string]any{}, "n", 0}, {map[string]any{}, "n", 0},
} }
for _, tt := range tests { for _, tt := range tests {
got := getInt(tt.args, tt.key) got := tools.GetInt(tt.args, tt.key)
if got != tt.want { if got != tt.want {
t.Errorf("getInt(%v, %q) = %d, want %d", tt.args, tt.key, got, tt.want) t.Errorf("GetInt(%v, %q) = %d, want %d", tt.args, tt.key, got, tt.want)
} }
} }
} }
func min(a, b int) int {
if a < b {
return a
}
return b
}
+13 -11
View File
@@ -1,8 +1,10 @@
package tools package matrix
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/enmanuel/agents/tools"
) )
// MatrixSender is the interface for sending Matrix messages. // MatrixSender is the interface for sending Matrix messages.
@@ -13,28 +15,28 @@ type MatrixSender interface {
} }
// NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room. // NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room.
func NewMatrixSend(sender MatrixSender) Tool { func NewMatrixSend(sender MatrixSender) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "matrix_send", Name: "matrix_send",
Description: "Send a text message to a Matrix room.", Description: "Send a text message to a Matrix room.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "room_id", Type: "string", Description: "The Matrix room ID to send to", Required: true}, {Name: "room_id", Type: "string", Description: "The Matrix room ID to send to", Required: true},
{Name: "message", Type: "string", Description: "The text message to send", Required: true}, {Name: "message", Type: "string", Description: "The text message to send", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
roomID := getString(args, "room_id") roomID := tools.GetString(args, "room_id")
message := getString(args, "message") message := tools.GetString(args, "message")
if roomID == "" || message == "" { if roomID == "" || message == "" {
return Result{Err: fmt.Errorf("matrix_send: room_id and message are required")} return tools.Result{Err: fmt.Errorf("matrix_send: room_id and message are required")}
} }
if err := sender.SendMarkdown(ctx, roomID, message); err != nil { if err := sender.SendMarkdown(ctx, roomID, message); err != nil {
return Result{Err: fmt.Errorf("matrix_send: %w", err)} return tools.Result{Err: fmt.Errorf("matrix_send: %w", err)}
} }
return Result{Output: fmt.Sprintf("message sent to %s", roomID)} return tools.Result{Output: fmt.Sprintf("message sent to %s", roomID)}
}, },
} }
} }
+52 -51
View File
@@ -1,4 +1,4 @@
package tools package memorytools
import ( import (
"context" "context"
@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/enmanuel/agents/pkg/memory" "github.com/enmanuel/agents/pkg/memory"
"github.com/enmanuel/agents/tools"
) )
// MemoryStore is the subset of memory.Store needed by memory tools. // MemoryStore is the subset of memory.Store needed by memory tools.
@@ -44,23 +45,23 @@ func (rc *RoomContext) Get() string {
} }
// NewMemorySave creates a tool that saves a fact to long-term memory. // NewMemorySave creates a tool that saves a fact to long-term memory.
func NewMemorySave(agentID string, store MemoryStore) Tool { func NewMemorySave(agentID string, store MemoryStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "memory_save", Name: "memory_save",
Description: "Save a fact to long-term memory. Use this to remember important information about users, topics, or preferences.", Description: "Save a fact to long-term memory. Use this to remember important information about users, topics, or preferences.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "subject", Type: "string", Description: "The subject this fact is about (e.g. a username, a topic)", Required: true}, {Name: "subject", Type: "string", Description: "The subject this fact is about (e.g. a username, a topic)", Required: true},
{Name: "key", Type: "string", Description: "The fact key (e.g. 'favorite_language', 'timezone')", Required: true}, {Name: "key", Type: "string", Description: "The fact key (e.g. 'favorite_language', 'timezone')", Required: true},
{Name: "value", Type: "string", Description: "The fact value to store", Required: true}, {Name: "value", Type: "string", Description: "The fact value to store", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
subject := getString(args, "subject") subject := tools.GetString(args, "subject")
key := getString(args, "key") key := tools.GetString(args, "key")
value := getString(args, "value") value := tools.GetString(args, "value")
if subject == "" || key == "" || value == "" { if subject == "" || key == "" || value == "" {
return Result{Err: fmt.Errorf("memory_save: subject, key, and value are required")} return tools.Result{Err: fmt.Errorf("memory_save: subject, key, and value are required")}
} }
err := store.SaveFact(ctx, memory.Fact{ err := store.SaveFact(ctx, memory.Fact{
AgentID: agentID, AgentID: agentID,
@@ -69,119 +70,119 @@ func NewMemorySave(agentID string, store MemoryStore) Tool {
Value: value, Value: value,
}) })
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("memory_save: %w", err)} return tools.Result{Err: fmt.Errorf("memory_save: %w", err)}
} }
return Result{Output: fmt.Sprintf("saved: %s.%s = %s", subject, key, value)} return tools.Result{Output: fmt.Sprintf("saved: %s.%s = %s", subject, key, value)}
}, },
} }
} }
// NewMemoryRecall creates a tool that retrieves facts from long-term memory. // NewMemoryRecall creates a tool that retrieves facts from long-term memory.
func NewMemoryRecall(agentID string, store MemoryStore) Tool { func NewMemoryRecall(agentID string, store MemoryStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "memory_recall", Name: "memory_recall",
Description: "Recall facts from long-term memory about a subject. Omit key to get all facts for the subject.", Description: "Recall facts from long-term memory about a subject. Omit key to get all facts for the subject.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "subject", Type: "string", Description: "The subject to recall facts about", Required: true}, {Name: "subject", Type: "string", Description: "The subject to recall facts about", Required: true},
{Name: "key", Type: "string", Description: "Optional specific fact key to recall", Required: false}, {Name: "key", Type: "string", Description: "Optional specific fact key to recall", Required: false},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
subject := getString(args, "subject") subject := tools.GetString(args, "subject")
if subject == "" { if subject == "" {
return Result{Err: fmt.Errorf("memory_recall: subject is required")} return tools.Result{Err: fmt.Errorf("memory_recall: subject is required")}
} }
var keyPtr *string var keyPtr *string
if k := getString(args, "key"); k != "" { if k := tools.GetString(args, "key"); k != "" {
keyPtr = &k keyPtr = &k
} }
facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr) facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("memory_recall: %w", err)} return tools.Result{Err: fmt.Errorf("memory_recall: %w", err)}
} }
if len(facts) == 0 { if len(facts) == 0 {
return Result{Output: fmt.Sprintf("no facts found for subject %q", subject)} return tools.Result{Output: fmt.Sprintf("no facts found for subject %q", subject)}
} }
var sb strings.Builder var sb strings.Builder
for _, f := range facts { for _, f := range facts {
fmt.Fprintf(&sb, "%s.%s = %s\n", f.Subject, f.Key, f.Value) fmt.Fprintf(&sb, "%s.%s = %s\n", f.Subject, f.Key, f.Value)
} }
return Result{Output: sb.String()} return tools.Result{Output: sb.String()}
}, },
} }
} }
// NewMemoryForget creates a tool that deletes facts from long-term memory. // NewMemoryForget creates a tool that deletes facts from long-term memory.
func NewMemoryForget(agentID string, store MemoryStore) Tool { func NewMemoryForget(agentID string, store MemoryStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "memory_forget", Name: "memory_forget",
Description: "Delete facts from long-term memory. Omit key to delete all facts for the subject.", Description: "Delete facts from long-term memory. Omit key to delete all facts for the subject.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "subject", Type: "string", Description: "The subject whose facts to delete", Required: true}, {Name: "subject", Type: "string", Description: "The subject whose facts to delete", Required: true},
{Name: "key", Type: "string", Description: "Optional specific fact key to delete; omit to delete all", Required: false}, {Name: "key", Type: "string", Description: "Optional specific fact key to delete; omit to delete all", Required: false},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
subject := getString(args, "subject") subject := tools.GetString(args, "subject")
if subject == "" { if subject == "" {
return Result{Err: fmt.Errorf("memory_forget: subject is required")} return tools.Result{Err: fmt.Errorf("memory_forget: subject is required")}
} }
var keyPtr *string var keyPtr *string
if k := getString(args, "key"); k != "" { if k := tools.GetString(args, "key"); k != "" {
keyPtr = &k keyPtr = &k
} }
err := store.DeleteFacts(ctx, agentID, subject, keyPtr) err := store.DeleteFacts(ctx, agentID, subject, keyPtr)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("memory_forget: %w", err)} return tools.Result{Err: fmt.Errorf("memory_forget: %w", err)}
} }
if keyPtr != nil { if keyPtr != nil {
return Result{Output: fmt.Sprintf("forgot %s.%s", subject, *keyPtr)} return tools.Result{Output: fmt.Sprintf("forgot %s.%s", subject, *keyPtr)}
} }
return Result{Output: fmt.Sprintf("forgot all facts about %s", subject)} return tools.Result{Output: fmt.Sprintf("forgot all facts about %s", subject)}
}, },
} }
} }
// NewMemoryClearContext creates a tool that clears the conversation window. // NewMemoryClearContext creates a tool that clears the conversation window.
func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) Tool { func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "memory_clear_context", Name: "memory_clear_context",
Description: "Clear the conversation context window. Useful to start fresh. Omit room_id to clear the current room.", Description: "Clear the conversation context window. Useful to start fresh. Omit room_id to clear the current room.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "room_id", Type: "string", Description: "Optional room ID to clear; defaults to current room", Required: false}, {Name: "room_id", Type: "string", Description: "Optional room ID to clear; defaults to current room", Required: false},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
roomID := getString(args, "room_id") roomID := tools.GetString(args, "room_id")
if roomID == "" { if roomID == "" {
roomID = roomCtx.Get() roomID = roomCtx.Get()
} }
if roomID == "" { if roomID == "" {
return Result{Err: fmt.Errorf("memory_clear_context: no room_id provided and no current room")} return tools.Result{Err: fmt.Errorf("memory_clear_context: no room_id provided and no current room")}
} }
clearer.ClearWindow(roomID) clearer.ClearWindow(roomID)
return Result{Output: fmt.Sprintf("conversation context cleared for room %s", roomID)} return tools.Result{Output: fmt.Sprintf("conversation context cleared for room %s", roomID)}
}, },
} }
} }
// NewMemorySummary creates a tool that saves an important summary to long-term memory. // NewMemorySummary creates a tool that saves an important summary to long-term memory.
func NewMemorySummary(agentID string, store MemoryStore) Tool { func NewMemorySummary(agentID string, store MemoryStore) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "memory_summary", Name: "memory_summary",
Description: "Save an important summary or takeaway from the current conversation to long-term memory.", Description: "Save an important summary or takeaway from the current conversation to long-term memory.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "text", Type: "string", Description: "The summary text to save", Required: true}, {Name: "text", Type: "string", Description: "The summary text to save", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
text := getString(args, "text") text := tools.GetString(args, "text")
if text == "" { if text == "" {
return Result{Err: fmt.Errorf("memory_summary: text is required")} return tools.Result{Err: fmt.Errorf("memory_summary: text is required")}
} }
key := time.Now().UTC().Format("2006-01-02T15:04:05") key := time.Now().UTC().Format("2006-01-02T15:04:05")
err := store.SaveFact(ctx, memory.Fact{ err := store.SaveFact(ctx, memory.Fact{
@@ -191,9 +192,9 @@ func NewMemorySummary(agentID string, store MemoryStore) Tool {
Value: text, Value: text,
}) })
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("memory_summary: %w", err)} return tools.Result{Err: fmt.Errorf("memory_summary: %w", err)}
} }
return Result{Output: "summary saved"} return tools.Result{Output: "summary saved"}
}, },
} }
} }
+15 -14
View File
@@ -1,4 +1,4 @@
package tools package ssh
import ( import (
"context" "context"
@@ -7,33 +7,34 @@ import (
"github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/internal/config"
corespecs "github.com/enmanuel/agents/pkg/tools" corespecs "github.com/enmanuel/agents/pkg/tools"
"github.com/enmanuel/agents/shell/ssh" shellssh "github.com/enmanuel/agents/shell/ssh"
"github.com/enmanuel/agents/tools"
) )
// NewSSHCommand creates an ssh_command tool that executes remote commands via SSH. // NewSSHCommand creates an ssh_command tool that executes remote commands via SSH.
// Validates targets against cfg.AllowedTargets and commands against cfg.ForbiddenCommands. // Validates targets against cfg.AllowedTargets and commands against cfg.ForbiddenCommands.
func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool { func NewSSHCommand(cfg config.SSHToolCfg, exec *shellssh.Executor) tools.Tool {
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "ssh_command", Name: "ssh_command",
Description: "Execute a command on a remote server via SSH.", Description: "Execute a command on a remote server via SSH.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "target", Type: "string", Description: "The SSH target name (e.g. production, staging)", Required: true}, {Name: "target", Type: "string", Description: "The SSH target name (e.g. production, staging)", Required: true},
{Name: "command", Type: "string", Description: "The shell command to execute", Required: true}, {Name: "command", Type: "string", Description: "The shell command to execute", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
target := getString(args, "target") target := tools.GetString(args, "target")
command := getString(args, "command") command := tools.GetString(args, "command")
if target == "" || command == "" { if target == "" || command == "" {
return Result{Err: fmt.Errorf("ssh_command: target and command are required")} return tools.Result{Err: fmt.Errorf("ssh_command: target and command are required")}
} }
if err := validateTarget(target, cfg.AllowedTargets); err != nil { if err := validateTarget(target, cfg.AllowedTargets); err != nil {
return Result{Err: err} return tools.Result{Err: err}
} }
if err := validateCommand(command, cfg.ForbiddenCommands); err != nil { if err := validateCommand(command, cfg.ForbiddenCommands); err != nil {
return Result{Err: err} return tools.Result{Err: err}
} }
timeout := "30s" timeout := "30s"
@@ -48,14 +49,14 @@ func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool {
}) })
if res.Err != nil { if res.Err != nil {
return Result{Err: fmt.Errorf("ssh_command: %w", res.Err)} return tools.Result{Err: fmt.Errorf("ssh_command: %w", res.Err)}
} }
output := res.Stdout output := res.Stdout
if res.Stderr != "" { if res.Stderr != "" {
output += "\nstderr: " + res.Stderr output += "\nstderr: " + res.Stderr
} }
return Result{Output: output} return tools.Result{Output: output}
}, },
} }
} }
+18 -2
View File
@@ -35,8 +35,8 @@ type Tool struct {
Exec ToolFunc Exec ToolFunc
} }
// getString extracts a string argument by name, returning "" if missing or wrong type. // GetString extracts a string argument by name, returning "" if missing or wrong type.
func getString(args map[string]any, key string) string { func GetString(args map[string]any, key string) string {
v, ok := args[key] v, ok := args[key]
if !ok { if !ok {
return "" return ""
@@ -47,3 +47,19 @@ func getString(args map[string]any, key string) string {
} }
return s return s
} }
// GetInt extracts an integer argument by name, returning 0 if missing or wrong type.
func GetInt(args map[string]any, key string) int {
v, ok := args[key]
if !ok {
return 0
}
switch n := v.(type) {
case float64:
return int(n)
case int:
return n
default:
return 0
}
}
+13 -11
View File
@@ -1,4 +1,4 @@
package tools package weather
import ( import (
"context" "context"
@@ -9,42 +9,44 @@ import (
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/enmanuel/agents/tools"
) )
// NewWeather creates a get_weather tool that fetches current weather and forecast // NewWeather creates a get_weather tool that fetches current weather and forecast
// for a city using the Open-Meteo API (free, no API key required). // for a city using the Open-Meteo API (free, no API key required).
func NewWeather() Tool { func NewWeather() tools.Tool {
client := &http.Client{Timeout: 15 * time.Second} client := &http.Client{Timeout: 15 * time.Second}
return Tool{ return tools.Tool{
Def: Def{ Def: tools.Def{
Name: "get_weather", Name: "get_weather",
Description: "Get current weather conditions and 3-day forecast for a city. Returns temperature, humidity, wind speed, and weather description.", Description: "Get current weather conditions and 3-day forecast for a city. Returns temperature, humidity, wind speed, and weather description.",
Parameters: []Param{ Parameters: []tools.Param{
{Name: "city", Type: "string", Description: "City name to look up (e.g. 'Madrid', 'New York', 'Tokyo')", Required: true}, {Name: "city", Type: "string", Description: "City name to look up (e.g. 'Madrid', 'New York', 'Tokyo')", Required: true},
}, },
}, },
Exec: func(ctx context.Context, args map[string]any) Result { Exec: func(ctx context.Context, args map[string]any) tools.Result {
city := getString(args, "city") city := tools.GetString(args, "city")
if city == "" { if city == "" {
return Result{Err: fmt.Errorf("get_weather: city is required")} return tools.Result{Err: fmt.Errorf("get_weather: city is required")}
} }
// Step 1: Geocode city name to coordinates // Step 1: Geocode city name to coordinates
lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city) lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("get_weather: geocoding failed: %w", err)} return tools.Result{Err: fmt.Errorf("get_weather: geocoding failed: %w", err)}
} }
// Step 2: Fetch weather data // Step 2: Fetch weather data
weather, err := fetchWeather(ctx, client, lat, lon) weather, err := fetchWeather(ctx, client, lat, lon)
if err != nil { if err != nil {
return Result{Err: fmt.Errorf("get_weather: forecast failed: %w", err)} return tools.Result{Err: fmt.Errorf("get_weather: forecast failed: %w", err)}
} }
// Step 3: Format output // Step 3: Format output
output := formatWeather(resolvedName, country, weather) output := formatWeather(resolvedName, country, weather)
return Result{Output: output} return tools.Result{Output: output}
}, },
} }
} }