返回 DeepSeek-Reasonix
tool.go
根目录 / internal / history / tool.go
1 package history
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "reasonix/internal/tool"
10 )
11
12 type historyTool struct {
13 searcher *Searcher
14 }
15
16 // NewTool returns a read-only history retrieval tool bound to local sessions.
17 func NewTool(opts Options) tool.Tool {
18 return historyTool{searcher: NewSearcher(opts)}
19 }
20
21 func (historyTool) Name() string { return "history" }
22
23 func (historyTool) Description() string {
24 return "Search saved local session history with lightweight BM25 retrieval, then read messages around a hit. " +
25 "Use search when past decisions, failed attempts, commands, or tool inputs may help the current task; use around with a returned session_path and message_index to inspect the nearby transcript. " +
26 "By default it searches user text, assistant text, tool inputs, and tool errors; normal tool outputs are excluded unless kind includes tool_output."
27 }
28
29 func (historyTool) Schema() json.RawMessage {
30 return json.RawMessage(`{
31 "type": "object",
32 "properties": {
33 "operation": {"type": "string", "enum": ["search", "around"], "description": "search ranks saved history; around returns nearby messages for a search hit."},
34 "query": {"type": "string", "description": "Search query for operation=search."},
35 "scope": {"type": "string", "enum": ["project", "global"], "description": "project searches the current session directory; global also includes compacted-history archives."},
36 "kind": {"type": "array", "items": {"type": "string", "enum": ["user_text", "assistant_text", "tool_input", "tool_error", "tool_output"]}, "description": "History parts to search. Defaults to user_text, assistant_text, tool_input, and tool_error."},
37 "tool_name": {"type": "string", "description": "Optional tool-name filter for tool_input, tool_error, or tool_output."},
38 "limit": {"type": "integer", "description": "Maximum search hits to return, default 8, max 20."},
39 "session_path": {"type": "string", "description": "Path from a search hit. Required for operation=around."},
40 "message_index": {"type": "integer", "description": "Message index from a search hit. Required for operation=around."},
41 "before": {"type": "integer", "description": "Messages before message_index for operation=around, default 3, max 10."},
42 "after": {"type": "integer", "description": "Messages after message_index for operation=around, default 3, max 10."}
43 },
44 "required": ["operation"]
45 }`)
46 }
47
48 func (t historyTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
49 var in struct {
50 Operation string `json:"operation"`
51 Query string `json:"query"`
52 Scope string `json:"scope"`
53 Kind []Kind `json:"kind"`
54 ToolName string `json:"tool_name"`
55 Limit int `json:"limit"`
56 SessionPath string `json:"session_path"`
57 MessageIndex *int `json:"message_index"`
58 Before int `json:"before"`
59 After int `json:"after"`
60 }
61 if err := json.Unmarshal(args, &in); err != nil {
62 return "", fmt.Errorf("invalid arguments: %w", err)
63 }
64 switch strings.TrimSpace(in.Operation) {
65 case "search":
66 hits, err := t.searcher.Search(ctx, SearchRequest{
67 Query: in.Query,
68 Scope: in.Scope,
69 Kinds: in.Kind,
70 ToolName: in.ToolName,
71 Limit: in.Limit,
72 })
73 if err != nil {
74 return "", err
75 }
76 return formatHits(in.Query, hits), nil
77 case "around":
78 if in.MessageIndex == nil {
79 return "", fmt.Errorf("message_index is required for operation=around")
80 }
81 msgs, err := t.searcher.Around(ctx, AroundRequest{
82 SessionPath: in.SessionPath,
83 MessageIndex: *in.MessageIndex,
84 Before: in.Before,
85 After: in.After,
86 })
87 if err != nil {
88 return "", err
89 }
90 return formatAround(in.SessionPath, *in.MessageIndex, msgs), nil
91 case "":
92 return "", fmt.Errorf("operation is required")
93 default:
94 return "", fmt.Errorf("unknown operation %q", in.Operation)
95 }
96 }
97
98 func (historyTool) ReadOnly() bool { return true }
99
100 func formatHits(query string, hits []Hit) string {
101 if len(hits) == 0 {
102 return strings.Join([]string{
103 "No saved session history matched " + strconvQuote(query) + ".",
104 "",
105 "0 results does not prove the event never happened. Try:",
106 "1. Retry with fewer, rarer terms such as a function name, command, error phrase, ticket id, or decision keyword.",
107 "2. Widen scope from project to global when cross-project or compacted-history context may matter.",
108 "3. If you need tool output, include kind=[\"tool_output\"] or filter by tool_name for tool input/error/output searches.",
109 }, "\n")
110 }
111 var b strings.Builder
112 fmt.Fprintf(&b, "History search results for %s:\n", strconvQuote(query))
113 for i, hit := range hits {
114 fmt.Fprintf(&b, "\n%d. score=%.3f source=%s session_id=%s message_index=%d kind=%s role=%s",
115 i+1, hit.Score, hit.Source, hit.SessionID, hit.MessageIndex, hit.Kind, hit.Role)
116 if hit.ToolName != "" {
117 fmt.Fprintf(&b, " tool=%s", hit.ToolName)
118 }
119 fmt.Fprintf(&b, "\n session_path: %s\n snippet: %s\n",
120 hit.SessionPath, hit.Snippet)
121 }
122 b.WriteString("\nUse operation=\"around\" with a session_path and message_index to read nearby messages.")
123 return strings.TrimSpace(b.String())
124 }
125
126 func formatAround(path string, idx int, msgs []MessageContext) string {
127 var b strings.Builder
128 fmt.Fprintf(&b, "History around %s message_index=%d:\n", path, idx)
129 for _, msg := range msgs {
130 fmt.Fprintf(&b, "\n%s\n", msg.Text)
131 }
132 return strings.TrimSpace(b.String())
133 }
134
135 func strconvQuote(s string) string {
136 b, _ := json.Marshal(s)
137 return string(b)
138 }
139
139 lines GO