package tools import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/nextlevelbuilder/goclaw/internal/store" ) // ============================================================ // sessions_list // ============================================================ type SessionsListTool struct { sessions store.SessionStore } func NewSessionsListTool() *SessionsListTool { return &SessionsListTool{} } func (t *SessionsListTool) SetSessionStore(s store.SessionStore) { t.sessions = s } func (t *SessionsListTool) Name() string { return "sessions_list" } func (t *SessionsListTool) Description() string { return "List sessions for this agent with optional filters." } func (t *SessionsListTool) Parameters() map[string]any { return map[string]any{ "type": "object", "properties": map[string]any{ "limit": map[string]any{ "type": "number", "description": "Max sessions to return (default 20)", }, "active_minutes": map[string]any{ "type": "number", "description": "Only show sessions active in the last N minutes", }, }, } } func (t *SessionsListTool) Execute(ctx context.Context, args map[string]any) *Result { if t.sessions == nil { return ErrorResult("session store not available") } limit := 20 if v, ok := args["limit"].(float64); ok && int(v) > 0 { limit = int(v) } var activeMinutes int if v, ok := args["active_minutes"].(float64); ok && int(v) > 0 { activeMinutes = int(v) } agentID := resolveAgentIDString(ctx) sessions := t.sessions.List(agentID) // Filter by active_minutes if activeMinutes > 0 { cutoff := time.Now().Add(-time.Duration(activeMinutes) * time.Minute) var filtered []store.SessionInfo for _, s := range sessions { if s.Updated.After(cutoff) { filtered = append(filtered, s) } } sessions = filtered } // Apply limit if len(sessions) > limit { sessions = sessions[:limit] } type sessionEntry struct { Key string `json:"key"` MessageCount int `json:"message_count"` Updated string `json:"updated"` } entries := make([]sessionEntry, 0, len(sessions)) for _, s := range sessions { entries = append(entries, sessionEntry{ Key: s.Key, MessageCount: s.MessageCount, Updated: s.Updated.Format(time.RFC3339), }) } out, _ := json.Marshal(map[string]any{ "count": len(entries), "sessions": entries, }) return SilentResult(string(out)) } // ============================================================ // session_status // ============================================================ type SessionStatusTool struct { sessions store.SessionStore } func NewSessionStatusTool() *SessionStatusTool { return &SessionStatusTool{} } func (t *SessionStatusTool) SetSessionStore(s store.SessionStore) { t.sessions = s } func (t *SessionStatusTool) Name() string { return "session_status" } func (t *SessionStatusTool) Description() string { return "Show session status: model, tokens, compaction count, channel, last update." } func (t *SessionStatusTool) Parameters() map[string]any { return map[string]any{ "type": "object", "properties": map[string]any{ "session_key": map[string]any{ "type": "string", "description": "Session key to inspect (default: current session)", }, }, } } func (t *SessionStatusTool) Execute(ctx context.Context, args map[string]any) *Result { if t.sessions == nil { return ErrorResult("session store not available") } sessionKey, _ := args["session_key"].(string) if sessionKey == "" { sessionKey = ToolSandboxKeyFromCtx(ctx) // sandboxKey == sessionKey in registry } if sessionKey == "" { return ErrorResult("session_key is required (could not detect current session)") } // Security: validate session belongs to current agent agentID := resolveAgentIDString(ctx) if agentID != "" && !strings.HasPrefix(sessionKey, "agent:"+agentID+":") { return ErrorResult("access denied: session belongs to a different agent") } data := t.sessions.GetOrCreate(sessionKey) var lines []string lines = append(lines, fmt.Sprintf("Session: %s", data.Key)) if data.Model != "" { lines = append(lines, fmt.Sprintf("Model: %s", data.Model)) } if data.Provider != "" { lines = append(lines, fmt.Sprintf("Provider: %s", data.Provider)) } if data.Channel != "" { lines = append(lines, fmt.Sprintf("Channel: %s", data.Channel)) } lines = append(lines, fmt.Sprintf("Messages: %d", len(data.Messages))) lines = append(lines, fmt.Sprintf("Tokens: %d input / %d output", data.InputTokens, data.OutputTokens)) lines = append(lines, fmt.Sprintf("Compactions: %d", data.CompactionCount)) if data.Summary != "" { lines = append(lines, fmt.Sprintf("Has summary: yes (%d chars)", len(data.Summary))) } if data.Label != "" { lines = append(lines, fmt.Sprintf("Label: %s", data.Label)) } lines = append(lines, fmt.Sprintf("Updated: %s", data.Updated.Format(time.RFC3339))) return SilentResult(strings.Join(lines, "\n")) }