174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"agentu/pkg/llm"
|
|
)
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// TodoTool — in-memory task tracking
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
type todoItem struct {
|
|
Content string `json:"content"`
|
|
Status string `json:"status"` // "pending", "in_progress", "completed"
|
|
}
|
|
|
|
type TodoTool struct {
|
|
mu sync.Mutex
|
|
items []todoItem
|
|
}
|
|
|
|
func NewTodoTool() *TodoTool {
|
|
return &TodoTool{}
|
|
}
|
|
|
|
func (t *TodoTool) Definition() llm.Tool {
|
|
return llm.Tool{
|
|
Type: "function",
|
|
Function: llm.ToolFunction{
|
|
Name: "todo",
|
|
Description: "Manage a task list for the current conversation. Create, update, and track multi-step work. Use this to plan complex tasks and show progress.",
|
|
Parameters: JSONSchema(`{
|
|
"type": "object",
|
|
"properties": {
|
|
"action": {"type": "string", "enum": ["list", "create", "update", "start", "complete", "clear"], "description": "Operation to perform."},
|
|
"items": {"type": "array", "items": {"type": "string"}, "description": "For 'create': list of task descriptions to add."},
|
|
"task": {"type": "string", "description": "For 'update'/'start'/'complete': the exact task content string to target."},
|
|
"status": {"type": "string", "enum": ["pending", "in_progress", "completed"], "description": "For 'update': new status for the task."}
|
|
},
|
|
"required": ["action"],
|
|
"additionalProperties": false
|
|
}`),
|
|
},
|
|
}
|
|
}
|
|
|
|
type todoArgs struct {
|
|
Action string `json:"action"`
|
|
Items []string `json:"items"`
|
|
Task string `json:"task"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (t *TodoTool) Execute(ctx context.Context, raw json.RawMessage) (string, error) {
|
|
var args todoArgs
|
|
if err := json.Unmarshal(raw, &args); err != nil {
|
|
return "", fmt.Errorf("parse arguments: %w", err)
|
|
}
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
switch args.Action {
|
|
case "list":
|
|
return t.listTasks(), nil
|
|
case "create":
|
|
return t.createTasks(args.Items)
|
|
case "start":
|
|
return t.startTask(args.Task)
|
|
case "complete":
|
|
return t.completeTask(args.Task)
|
|
case "update":
|
|
return t.updateTask(args.Task, args.Status)
|
|
case "clear":
|
|
t.items = nil
|
|
return "Tasks cleared.", nil
|
|
default:
|
|
return "", fmt.Errorf("unknown todo action: %s", args.Action)
|
|
}
|
|
}
|
|
|
|
func (t *TodoTool) listTasks() string {
|
|
if len(t.items) == 0 {
|
|
return "No tasks."
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("Tasks:\n")
|
|
for _, item := range t.items {
|
|
fmt.Fprintf(&b, "[%s] %s\n", item.Status, item.Content)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (t *TodoTool) createTasks(items []string) (string, error) {
|
|
if len(items) == 0 {
|
|
return "", fmt.Errorf("items is required for create")
|
|
}
|
|
for _, content := range items {
|
|
content = strings.TrimSpace(content)
|
|
if content == "" {
|
|
return "", fmt.Errorf("task content must not be empty")
|
|
}
|
|
t.items = append(t.items, todoItem{
|
|
Content: content,
|
|
Status: "pending",
|
|
})
|
|
}
|
|
return t.listTasks(), nil
|
|
}
|
|
|
|
func (t *TodoTool) startTask(task string) (string, error) {
|
|
idx := -1
|
|
for i, item := range t.items {
|
|
if item.Content == task {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return "", fmt.Errorf("task not found: %s", task)
|
|
}
|
|
|
|
// Set all in_progress items to pending, then mark the target as in_progress.
|
|
for i := range t.items {
|
|
if t.items[i].Status == "in_progress" {
|
|
t.items[i].Status = "pending"
|
|
}
|
|
}
|
|
t.items[idx].Status = "in_progress"
|
|
return t.listTasks(), nil
|
|
}
|
|
|
|
func (t *TodoTool) completeTask(task string) (string, error) {
|
|
idx := -1
|
|
for i, item := range t.items {
|
|
if item.Content == task {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return "", fmt.Errorf("task not found: %s", task)
|
|
}
|
|
t.items[idx].Status = "completed"
|
|
return t.listTasks(), nil
|
|
}
|
|
|
|
func (t *TodoTool) updateTask(task, status string) (string, error) {
|
|
if status != "pending" && status != "in_progress" && status != "completed" {
|
|
return "", fmt.Errorf("status must be one of: pending, in_progress, completed")
|
|
}
|
|
idx := -1
|
|
for i, item := range t.items {
|
|
if item.Content == task {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return "", fmt.Errorf("task not found: %s", task)
|
|
}
|
|
t.items[idx].Status = status
|
|
return t.listTasks(), nil
|
|
}
|