package builtin import ( "context" "encoding/json" "fmt" "strings" "sync" "nub/internal/tool" ) // TodoTool hält eine In-Memory-Todo-Liste im Session-State (5.3) — nicht im // Baum persistiert. Zustand lebt im Tool selbst, deshalb Pointer-Registrierung // (&TodoTool{}) statt eines Werttyps. type TodoTool struct { mu sync.Mutex items []TodoItem } type TodoItem struct { Content string `json:"content"` Status string `json:"status"` // pending | in_progress | completed } func (*TodoTool) Name() string { return "todo" } func (*TodoTool) Description() string { return "Ersetzt die aktuelle Todo-Liste. Für lange Tasks: Fortschritt sichtbar halten." } func (*TodoTool) Schema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]} }, "required": ["content", "status"] } } }, "required": ["items"] }`) } type todoInput struct { Items []TodoItem `json:"items"` } func (t *TodoTool) Run(ctx context.Context, input json.RawMessage, env tool.Env) (tool.Result, error) { var in todoInput if err := json.Unmarshal(input, &in); err != nil { return tool.Result{ForModel: "invalid input: " + err.Error(), IsError: true}, nil } for _, it := range in.Items { switch it.Status { case "pending", "in_progress", "completed": default: return tool.Result{ForModel: fmt.Sprintf("invalid status %q", it.Status), IsError: true}, nil } } t.mu.Lock() t.items = in.Items items := make([]TodoItem, len(t.items)) copy(items, t.items) t.mu.Unlock() return tool.Result{ForModel: renderTodos(items), ForUI: TodoResult{Items: items}}, nil } type TodoResult struct { Items []TodoItem } func renderTodos(items []TodoItem) string { if len(items) == 0 { return "todo list is empty" } var b strings.Builder for _, it := range items { mark := " " switch it.Status { case "in_progress": mark = "~" case "completed": mark = "x" } fmt.Fprintf(&b, "[%s] %s\n", mark, it.Content) } return b.String() }