返回 DeepSeek-Reasonix
title.go
根目录 / internal / tool / sessiontool / title.go
1 package sessiontool
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "path/filepath"
8 "strings"
9
10 "reasonix/internal/agent"
11 "reasonix/internal/store"
12 "reasonix/internal/tool"
13 )
14
15 // TitleChangedFunc projects a successful canonical title write into optional
16 // host-owned compatibility indexes. sessionDir is the boot-scoped root that
17 // owns sessionPath; callers must keep the projection inside that boundary.
18 type TitleChangedFunc func(sessionDir, sessionPath, title string) error
19
20 type setSessionTitleTool struct {
21 sessionDir string
22 currentSessionPath func() string
23 onTitleChanged TitleChangedFunc
24 currentSessionID func() string
25 writeEventTitle func(context.Context, string) error
26 }
27
28 // NewSetSessionTitleEventTool writes through the final session event owner.
29 func NewSetSessionTitleEventTool(currentSessionID func() string, write func(context.Context, string) error) *setSessionTitleTool {
30 return &setSessionTitleTool{currentSessionID: currentSessionID, writeEventTitle: write}
31 }
32
33 const setSessionTitleMaxRunes = 120
34
35 // NewSetSessionTitleTool creates a host-bound title writer for the current
36 // conversation. The model never supplies a path, so it cannot rename another
37 // tab or turn a read-oriented session path resolver into a file-write surface.
38 func NewSetSessionTitleTool(sessionDir string, currentSessionPath func() string, onTitleChanged TitleChangedFunc) *setSessionTitleTool {
39 return &setSessionTitleTool{
40 sessionDir: sessionDir,
41 currentSessionPath: currentSessionPath,
42 onTitleChanged: onTitleChanged,
43 }
44 }
45
46 func (t *setSessionTitleTool) Name() string { return tool.HostSetSessionTitle }
47 func (t *setSessionTitleTool) ReadOnly() bool { return false }
48 func (t *setSessionTitleTool) Description() string {
49 return "Set or clear the current conversation's saved title. Pass an empty title to fall back to the topic title or first-message preview. The host binds the current session; this tool cannot rename other sessions."
50 }
51
52 func (t *setSessionTitleTool) Schema() json.RawMessage {
53 return json.RawMessage(`{
54 "type": "object",
55 "properties": {
56 "title": {
57 "type": "string",
58 "maxLength": 120,
59 "description": "Short title for the current conversation. Empty clears the explicit session title."
60 }
61 },
62 "required": ["title"]
63 }`)
64 }
65
66 func (t *setSessionTitleTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
67 if agent.SubagentDepth(ctx) > 0 {
68 return "", fmt.Errorf("set_session_title: only the parent conversation can rename itself")
69 }
70 var params struct {
71 Title *string `json:"title"`
72 }
73 if err := json.Unmarshal(args, &params); err != nil {
74 return "", fmt.Errorf("set_session_title: invalid args: %w", err)
75 }
76 if params.Title == nil {
77 return "", fmt.Errorf("set_session_title: 'title' argument is required")
78 }
79 title := strings.TrimSpace(*params.Title)
80 if len([]rune(title)) > setSessionTitleMaxRunes {
81 return "", fmt.Errorf("set_session_title: title exceeds %d characters", setSessionTitleMaxRunes)
82 }
83 if t != nil && t.writeEventTitle != nil {
84 if t.currentSessionID == nil || strings.TrimSpace(t.currentSessionID()) == "" {
85 return "", fmt.Errorf("set_session_title: current session is unavailable")
86 }
87 if err := t.writeEventTitle(ctx, title); err != nil {
88 return "", fmt.Errorf("set_session_title: %w", err)
89 }
90 if title == "" {
91 return "Cleared the current conversation title.", nil
92 }
93 return fmt.Sprintf("Set the current conversation title to %q.", title), nil
94 }
95 if t == nil || t.currentSessionPath == nil {
96 return "", fmt.Errorf("set_session_title: current session is unavailable")
97 }
98 sessionPath := strings.TrimSpace(t.currentSessionPath())
99 if sessionPath == "" {
100 return "", fmt.Errorf("set_session_title: current session has no persistent path")
101 }
102 if !store.IsSessionTranscriptName(filepath.Base(sessionPath)) {
103 return "", fmt.Errorf("set_session_title: current session path is not a transcript")
104 }
105 if agent.IsCleanupPending(sessionPath) {
106 return "", fmt.Errorf("set_session_title: current session is pending cleanup")
107 }
108 if err := agent.RenameSession(sessionPath, title); err != nil {
109 return "", fmt.Errorf("set_session_title: %w", err)
110 }
111 if t.onTitleChanged != nil {
112 if err := t.onTitleChanged(t.sessionDir, sessionPath, title); err != nil {
113 return "", fmt.Errorf("set_session_title: update host title index: %w", err)
114 }
115 }
116 if title == "" {
117 return "Cleared the current conversation title.", nil
118 }
119 return fmt.Sprintf("Set the current conversation title to %q.", title), nil
120 }
121
121 lines GO