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 (
"context"
"fmt"
"time"
"github.com/enmanuel/agents/tools"
)
// NewCurrentTime creates a current_time tool that returns the current date and time.
// Useful for agents that need temporal awareness.
func NewCurrentTime() Tool {
return Tool{
Def: Def{
func NewCurrentTime() tools.Tool {
return tools.Tool{
Def: tools.Def{
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.",
Parameters: []Param{
Parameters: []tools.Param{
{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 {
layout := getString(args, "format")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
layout := tools.GetString(args, "format")
if layout == "" {
layout = time.RFC3339
}
now := time.Now()
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 (
"context"
@@ -8,37 +8,38 @@ import (
"strings"
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/tools"
)
// NewReadFile creates a read_file tool that reads local files.
// Validates paths against cfg.AllowedPaths when non-empty.
func NewReadFile(cfg config.FileOpsCfg) Tool {
return Tool{
Def: Def{
func NewReadFile(cfg config.FileOpsCfg) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "read_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},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
path := getString(args, "path")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
path := tools.GetString(args, "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)
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 {
return Result{Err: err}
return tools.Result{Err: err}
}
data, err := os.ReadFile(absPath)
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
@@ -47,7 +48,7 @@ func NewReadFile(cfg config.FileOpsCfg) Tool {
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 (
"context"
@@ -10,104 +10,105 @@ import (
"time"
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/tools"
)
// NewHTTPGet creates an http_get tool that performs GET requests.
// Validates URLs against cfg.AllowedDomains when non-empty.
func NewHTTPGet(cfg config.HTTPToolCfg) Tool {
func NewHTTPGet(cfg config.HTTPToolCfg) tools.Tool {
timeout := cfg.Timeout
if timeout == 0 {
timeout = 30 * time.Second
}
client := &http.Client{Timeout: timeout}
return Tool{
Def: Def{
return tools.Tool{
Def: tools.Def{
Name: "http_get",
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},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
rawURL := getString(args, "url")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
rawURL := tools.GetString(args, "url")
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 {
return Result{Err: err}
return tools.Result{Err: err}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, 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)
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()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64 KB limit
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.
// Validates URLs against cfg.AllowedDomains when non-empty.
func NewHTTPPost(cfg config.HTTPToolCfg) Tool {
func NewHTTPPost(cfg config.HTTPToolCfg) tools.Tool {
timeout := cfg.Timeout
if timeout == 0 {
timeout = 30 * time.Second
}
client := &http.Client{Timeout: timeout}
return Tool{
Def: Def{
return tools.Tool{
Def: tools.Def{
Name: "http_post",
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: "body", Type: "string", Description: "The JSON body to send", Required: true},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
rawURL := getString(args, "url")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
rawURL := tools.GetString(args, "url")
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 == "" {
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 {
return Result{Err: err}
return tools.Result{Err: err}
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, strings.NewReader(bodyStr))
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")
resp, err := client.Do(req)
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()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
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 (
"context"
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/enmanuel/agents/pkg/knowledge"
"github.com/enmanuel/agents/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.
func NewKnowledgeSearch(store KnowledgeStore) Tool {
return Tool{
Def: Def{
func NewKnowledgeSearch(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_search",
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: "limit", Type: "integer", Description: "Max results (default 5)", Required: false},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
query := getString(args, "query")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
query := tools.GetString(args, "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 {
limit = 5
}
results, err := store.Search(ctx, query, limit)
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 {
return Result{Output: "no documents found matching your query"}
return tools.Result{Output: "no documents found matching your query"}
}
var sb strings.Builder
for i, r := range results {
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.
func NewKnowledgeRead(store KnowledgeStore) Tool {
return Tool{
Def: Def{
func NewKnowledgeRead(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_read",
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},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
slug := getString(args, "slug")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
slug := tools.GetString(args, "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)
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.
func NewKnowledgeWrite(store KnowledgeStore) Tool {
return Tool{
Def: Def{
func NewKnowledgeWrite(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_write",
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: "content", Type: "string", Description: "Full markdown content of the document", Required: true},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
slug := getString(args, "slug")
content := getString(args, "content")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
slug := tools.GetString(args, "slug")
content := tools.GetString(args, "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{
@@ -102,28 +103,28 @@ func NewKnowledgeWrite(store KnowledgeStore) Tool {
Content: content,
})
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.
func NewKnowledgeList(store KnowledgeStore) Tool {
return Tool{
Def: Def{
func NewKnowledgeList(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_list",
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)
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 {
return Result{Output: "knowledge base is empty"}
return tools.Result{Output: "knowledge base is empty"}
}
var sb strings.Builder
@@ -131,23 +132,7 @@ func NewKnowledgeList(store KnowledgeStore) Tool {
fmt.Fprintf(&sb, "- `%s`: %s (updated %s)\n",
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 (
"context"
"testing"
"github.com/enmanuel/agents/pkg/knowledge"
"github.com/enmanuel/agents/tools"
)
// mockKnowledgeStore implements KnowledgeStore for testing.
@@ -173,16 +174,9 @@ func TestGetInt(t *testing.T) {
{map[string]any{}, "n", 0},
}
for _, tt := range tests {
got := getInt(tt.args, tt.key)
got := tools.GetInt(tt.args, tt.key)
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 (
"context"
"fmt"
"github.com/enmanuel/agents/tools"
)
// 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.
func NewMatrixSend(sender MatrixSender) Tool {
return Tool{
Def: Def{
func NewMatrixSend(sender MatrixSender) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "matrix_send",
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: "message", Type: "string", Description: "The text message to send", Required: true},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
roomID := getString(args, "room_id")
message := getString(args, "message")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
roomID := tools.GetString(args, "room_id")
message := tools.GetString(args, "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 {
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 (
"context"
@@ -8,6 +8,7 @@ import (
"time"
"github.com/enmanuel/agents/pkg/memory"
"github.com/enmanuel/agents/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.
func NewMemorySave(agentID string, store MemoryStore) Tool {
return Tool{
Def: Def{
func NewMemorySave(agentID string, store MemoryStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "memory_save",
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: "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},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
subject := getString(args, "subject")
key := getString(args, "key")
value := getString(args, "value")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
subject := tools.GetString(args, "subject")
key := tools.GetString(args, "key")
value := tools.GetString(args, "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{
AgentID: agentID,
@@ -69,119 +70,119 @@ func NewMemorySave(agentID string, store MemoryStore) Tool {
Value: value,
})
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.
func NewMemoryRecall(agentID string, store MemoryStore) Tool {
return Tool{
Def: Def{
func NewMemoryRecall(agentID string, store MemoryStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "memory_recall",
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: "key", Type: "string", Description: "Optional specific fact key to recall", Required: false},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
subject := getString(args, "subject")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
subject := tools.GetString(args, "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
if k := getString(args, "key"); k != "" {
if k := tools.GetString(args, "key"); k != "" {
keyPtr = &k
}
facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr)
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 {
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
for _, f := range facts {
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.
func NewMemoryForget(agentID string, store MemoryStore) Tool {
return Tool{
Def: Def{
func NewMemoryForget(agentID string, store MemoryStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "memory_forget",
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: "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 {
subject := getString(args, "subject")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
subject := tools.GetString(args, "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
if k := getString(args, "key"); k != "" {
if k := tools.GetString(args, "key"); k != "" {
keyPtr = &k
}
err := store.DeleteFacts(ctx, agentID, subject, keyPtr)
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 {
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.
func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) Tool {
return Tool{
Def: Def{
func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "memory_clear_context",
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},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
roomID := getString(args, "room_id")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
roomID := tools.GetString(args, "room_id")
if roomID == "" {
roomID = roomCtx.Get()
}
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)
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.
func NewMemorySummary(agentID string, store MemoryStore) Tool {
return Tool{
Def: Def{
func NewMemorySummary(agentID string, store MemoryStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "memory_summary",
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},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
text := getString(args, "text")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
text := tools.GetString(args, "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")
err := store.SaveFact(ctx, memory.Fact{
@@ -191,9 +192,9 @@ func NewMemorySummary(agentID string, store MemoryStore) Tool {
Value: text,
})
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 (
"context"
@@ -7,33 +7,34 @@ import (
"github.com/enmanuel/agents/internal/config"
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.
// Validates targets against cfg.AllowedTargets and commands against cfg.ForbiddenCommands.
func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool {
return Tool{
Def: Def{
func NewSSHCommand(cfg config.SSHToolCfg, exec *shellssh.Executor) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "ssh_command",
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: "command", Type: "string", Description: "The shell command to execute", Required: true},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
target := getString(args, "target")
command := getString(args, "command")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
target := tools.GetString(args, "target")
command := tools.GetString(args, "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 {
return Result{Err: err}
return tools.Result{Err: err}
}
if err := validateCommand(command, cfg.ForbiddenCommands); err != nil {
return Result{Err: err}
return tools.Result{Err: err}
}
timeout := "30s"
@@ -48,14 +49,14 @@ func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool {
})
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
if 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
}
// getString extracts a string argument by name, returning "" if missing or wrong type.
func getString(args map[string]any, key string) string {
// GetString extracts a string argument by name, returning "" if missing or wrong type.
func GetString(args map[string]any, key string) string {
v, ok := args[key]
if !ok {
return ""
@@ -47,3 +47,19 @@ func getString(args map[string]any, key string) string {
}
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 (
"context"
@@ -9,42 +9,44 @@ import (
"net/url"
"strings"
"time"
"github.com/enmanuel/agents/tools"
)
// 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).
func NewWeather() Tool {
func NewWeather() tools.Tool {
client := &http.Client{Timeout: 15 * time.Second}
return Tool{
Def: Def{
return tools.Tool{
Def: tools.Def{
Name: "get_weather",
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},
},
},
Exec: func(ctx context.Context, args map[string]any) Result {
city := getString(args, "city")
Exec: func(ctx context.Context, args map[string]any) tools.Result {
city := tools.GetString(args, "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
lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city)
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
weather, err := fetchWeather(ctx, client, lat, lon)
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
output := formatWeather(resolvedName, country, weather)
return Result{Output: output}
return tools.Result{Output: output}
},
}
}