返回 DeepSeek-Reasonix
todo.go
根目录 / internal / tool / builtin / todo.go
1 package builtin
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "strings"
10
11 "reasonix/internal/tool"
12 )
13
14 func init() { tool.RegisterBuiltin(todoWrite{}) }
15
16 // todoWrite replaces the current turn's complete, flat task list. The list is
17 // model-managed progress UI; it is deliberately independent from Plan and Goal
18 // authorization and from delivery evidence.
19 type todoWrite struct{}
20
21 type todoItem struct {
22 Content string `json:"content"`
23 Status string `json:"status"`
24 }
25
26 type todoWriteResponse struct {
27 Todos []todoItem `json:"todos"`
28 Counts struct {
29 Total int `json:"total"`
30 Pending int `json:"pending"`
31 InProgress int `json:"in_progress"`
32 Completed int `json:"completed"`
33 } `json:"counts"`
34 }
35
36 func (todoWrite) Name() string { return "todo_write" }
37
38 func (todoWrite) Description() string {
39 return "Replace the current turn's complete task list. Send the full flat list on every call; an empty list clears it. Items may be reordered, removed, replanned, or have any number in progress. Each item contains only content and status (pending|in_progress|completed)."
40 }
41
42 func (todoWrite) Schema() json.RawMessage {
43 return json.RawMessage(`{
44 "type":"object",
45 "additionalProperties":false,
46 "properties":{
47 "todos":{
48 "type":"array",
49 "description":"The complete flat task list for this turn. Replaces the previous list; [] clears it.",
50 "items":{
51 "type":"object",
52 "additionalProperties":false,
53 "properties":{
54 "content":{"type":"string","minLength":1,"description":"Task text. Leading and trailing whitespace is removed."},
55 "status":{"type":"string","enum":["pending","in_progress","completed"]}
56 },
57 "required":["content","status"]
58 }
59 }
60 },
61 "required":["todos"]
62 }`)
63 }
64
65 func (todoWrite) ReadOnly() bool { return true }
66
67 func (todoWrite) Execute(_ context.Context, args json.RawMessage) (string, error) {
68 var p struct {
69 Todos *[]json.RawMessage `json:"todos"`
70 }
71 dec := json.NewDecoder(bytes.NewReader(args))
72 dec.DisallowUnknownFields()
73 if err := dec.Decode(&p); err != nil {
74 return "", fmt.Errorf("invalid todo_write arguments: %w; send only {todos:[{content,status}]}", err)
75 }
76 if err := ensureJSONEnd(dec); err != nil {
77 return "", fmt.Errorf("invalid todo_write arguments: %w", err)
78 }
79 if p.Todos == nil {
80 return "", fmt.Errorf("todos is required and must be an array")
81 }
82
83 rawTodos := *p.Todos
84 response := todoWriteResponse{Todos: make([]todoItem, len(rawTodos))}
85 seen := make(map[string]int, len(rawTodos))
86 for i, raw := range rawTodos {
87 if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
88 return "", fmt.Errorf("todos[%d] must be an object with content and status", i)
89 }
90 var item todoItem
91 itemDecoder := json.NewDecoder(bytes.NewReader(raw))
92 itemDecoder.DisallowUnknownFields()
93 if err := itemDecoder.Decode(&item); err != nil {
94 return "", fmt.Errorf("todos[%d] is invalid: %w; use only content and status", i, err)
95 }
96 if err := ensureJSONEnd(itemDecoder); err != nil {
97 return "", fmt.Errorf("todos[%d] is invalid: %w", i, err)
98 }
99 item.Content = strings.TrimSpace(item.Content)
100 if item.Content == "" {
101 return "", fmt.Errorf("todos[%d].content must be non-empty after trimming", i)
102 }
103 if previous, ok := seen[item.Content]; ok {
104 return "", fmt.Errorf("todos[%d].content duplicates todos[%d].content %q; merge or rename one item", i, previous, item.Content)
105 }
106 seen[item.Content] = i
107 switch item.Status {
108 case "pending":
109 response.Counts.Pending++
110 case "in_progress":
111 response.Counts.InProgress++
112 case "completed":
113 response.Counts.Completed++
114 default:
115 return "", fmt.Errorf("todos[%d].status %q is invalid; use pending, in_progress, or completed", i, item.Status)
116 }
117 response.Todos[i] = item
118 }
119 response.Counts.Total = len(response.Todos)
120 out, err := json.Marshal(response)
121 if err != nil {
122 return "", fmt.Errorf("encode todo_write result: %w", err)
123 }
124 return string(out), nil
125 }
126
127 func ensureJSONEnd(dec *json.Decoder) error {
128 var extra any
129 if err := dec.Decode(&extra); err == io.EOF {
130 return nil
131 } else if err != nil {
132 return err
133 }
134 return fmt.Errorf("multiple JSON values are not allowed")
135 }
136
136 lines GO