blob: 5c23457ce8530fee98bdb01ba3061bd59d624816 [file] [log] [blame]
package claudetool
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sketch.dev/llm"
)
var TodoRead = &llm.Tool{
Name: "todo_read",
Description: `Reads the current todo list. Use frequently to track progress and understand what's pending.`,
InputSchema: llm.EmptySchema(),
Run: todoReadRun,
}
var TodoWrite = &llm.Tool{
Name: "todo_write",
Description: todoWriteDescription,
InputSchema: llm.MustSchema(todoWriteInputSchema),
Run: todoWriteRun,
}
const (
todoWriteDescription = `todo_write: Creates and manages a structured task list for tracking work and communicating progress to users. Use early and often.
Use for:
- multi-step tasks
- complex work
- when users provide multiple requests
- conversations that start trivial but grow in scope
- when users request additional work (directly or via feedback)
Skip for:
- trivial single-step tasks
- purely conversational exchanges
Update dynamically as work evolves - conversations can spawn tasks, simple tasks can become complex, and new discoveries may require additional work.
Rules:
- Update immediately when task states or task list changes
- Only one task "in-progress" at any time
- Each update completely replaces the task list - include all tasks (past and present)
- Never modify or delete completed tasks
- Queued and in-progress tasks may be restructured as understanding evolves
- Tasks should be atomic, clear, precise, and actionable
- If the user adds new tasks: append, don't replace
`
todoWriteInputSchema = `
{
"type": "object",
"required": ["tasks"],
"properties": {
"tasks": {
"type": "array",
"description": "Array of tasks to write",
"items": {
"type": "object",
"required": ["id", "task", "status"],
"properties": {
"id": {
"type": "string",
"description": "stable, unique hyphenated slug"
},
"task": {
"type": "string",
"description": "actionable step in active tense, sentence case, plain text only, displayed to user"
},
"status": {
"type": "string",
"enum": ["queued", "in-progress", "completed"],
"description": "current task status"
}
}
}
}
}
}
`
)
type TodoItem struct {
ID string `json:"id"`
Task string `json:"task"`
Status string `json:"status"`
}
type TodoList struct {
Items []TodoItem `json:"items"`
}
type TodoWriteInput struct {
Tasks []TodoItem `json:"tasks"`
}
// TodoFilePath returns the path to the todo file for the given session ID.
func TodoFilePath(sessionID string) string {
if sessionID == "" {
return "/tmp/sketch_todos.json"
}
return filepath.Join("/tmp", sessionID, "todos.json")
}
func todoFilePathForContext(ctx context.Context) string {
return TodoFilePath(SessionID(ctx))
}
func todoReadRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
todoPath := todoFilePathForContext(ctx)
content, err := os.ReadFile(todoPath)
if os.IsNotExist(err) {
return llm.ToolOut{LLMContent: llm.TextContent("No todo list found. Use todo_write to create one.")}
}
if err != nil {
return llm.ErrorfToolOut("failed to read todo file: %w", err)
}
var todoList TodoList
if err := json.Unmarshal(content, &todoList); err != nil {
return llm.ErrorfToolOut("failed to parse todo file: %w", err)
}
result := fmt.Sprintf(`<todo_list count="%d">%s`, len(todoList.Items), "\n")
for _, item := range todoList.Items {
result += fmt.Sprintf(` <task id="%s" status="%s">%s</task>%s`, item.ID, item.Status, item.Task, "\n")
}
result += "</todo_list>"
return llm.ToolOut{LLMContent: llm.TextContent(result)}
}
func todoWriteRun(ctx context.Context, m json.RawMessage) llm.ToolOut {
var input TodoWriteInput
if err := json.Unmarshal(m, &input); err != nil {
return llm.ErrorfToolOut("invalid input: %w", err)
}
// Validate that only one task is in-progress
inProgressCount := 0
for _, task := range input.Tasks {
if task.Status == "in-progress" {
inProgressCount++
}
}
switch {
case inProgressCount > 1:
return llm.ErrorfToolOut("only one task can be 'in-progress' at a time, found %d", inProgressCount)
}
todoList := TodoList{
Items: input.Tasks,
}
todoPath := todoFilePathForContext(ctx)
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(todoPath), 0o700); err != nil {
return llm.ErrorfToolOut("failed to create todo directory: %w", err)
}
content, err := json.Marshal(todoList)
if err != nil {
return llm.ErrorfToolOut("failed to marshal todo list: %w", err)
}
if err := os.WriteFile(todoPath, content, 0o600); err != nil {
return llm.ErrorfToolOut("failed to write todo file: %w", err)
}
result := fmt.Sprintf("Updated todo list with %d items.", len(input.Tasks))
return llm.ToolOut{LLMContent: llm.TextContent(result)}
}