From 5f8678f0914cc86f728bdb5d0642285a39850d1e Mon Sep 17 00:00:00 2001 From: DBT Date: Mon, 23 Feb 2026 13:13:06 +0000 Subject: [PATCH] add sessions tool and subagents steer control --- pkg/agent/loop.go | 31 ++++++++++- pkg/session/manager.go | 23 ++++++++ pkg/tools/sessions_tool.go | 101 ++++++++++++++++++++++++++++++++++++ pkg/tools/subagent.go | 18 +++++++ pkg/tools/subagents_tool.go | 17 ++++-- 5 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 pkg/tools/sessions_tool.go diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index a142c16..406ba35 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -51,6 +51,8 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers workspace := cfg.WorkspacePath() os.MkdirAll(workspace, 0755) + sessionsManager := session.NewSessionManager(filepath.Join(filepath.Dir(cfg.WorkspacePath()), "sessions")) + toolsRegistry := tools.NewToolRegistry() toolsRegistry.Register(&tools.ReadFileTool{}) toolsRegistry.Register(&tools.WriteFileTool{}) @@ -98,6 +100,19 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers spawnTool := tools.NewSpawnTool(subagentManager) toolsRegistry.Register(spawnTool) toolsRegistry.Register(tools.NewSubagentsTool(subagentManager)) + toolsRegistry.Register(tools.NewSessionsTool( + func(limit int) []tools.SessionInfo { + sessions := alSessionListForTool(sessionsManager, limit) + return sessions + }, + func(key string, limit int) []providers.Message { + h := sessionsManager.GetHistory(key) + if limit > 0 && len(h) > limit { + return h[len(h)-limit:] + } + return h + }, + )) // Register edit file tool editFileTool := tools.NewEditFileTool(workspace) @@ -120,8 +135,6 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers // Register system info tool toolsRegistry.Register(tools.NewSystemInfoTool()) - sessionsManager := session.NewSessionManager(filepath.Join(filepath.Dir(cfg.WorkspacePath()), "sessions")) - loop := &AgentLoop{ bus: msgBus, provider: provider, @@ -792,3 +805,17 @@ func truncateString(s string, maxLen int) string { } return s[:maxLen-3] + "..." } + +func alSessionListForTool(sm *session.SessionManager, limit int) []tools.SessionInfo { + items := sm.List(limit) + out := make([]tools.SessionInfo, 0, len(items)) + for _, s := range items { + out = append(out, tools.SessionInfo{ + Key: s.Key, + Kind: s.Kind, + Summary: s.Summary, + UpdatedAt: s.Updated, + }) + } + return out +} diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 4afe264..b985c46 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -250,6 +250,29 @@ func (sm *SessionManager) Keys() []string { return keys } +func (sm *SessionManager) List(limit int) []Session { + sm.mu.RLock() + defer sm.mu.RUnlock() + items := make([]Session, 0, len(sm.sessions)) + for _, s := range sm.sessions { + s.mu.RLock() + items = append(items, Session{ + Key: s.Key, + Kind: s.Kind, + Summary: s.Summary, + LastLanguage: s.LastLanguage, + PreferredLanguage: s.PreferredLanguage, + Created: s.Created, + Updated: s.Updated, + }) + s.mu.RUnlock() + } + if limit > 0 && len(items) > limit { + return items[:limit] + } + return items +} + func detectSessionKind(key string) string { k := strings.TrimSpace(strings.ToLower(key)) switch { diff --git a/pkg/tools/sessions_tool.go b/pkg/tools/sessions_tool.go new file mode 100644 index 0000000..1caf8d1 --- /dev/null +++ b/pkg/tools/sessions_tool.go @@ -0,0 +1,101 @@ +package tools + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + "clawgo/pkg/providers" +) + +type SessionInfo struct { + Key string + Kind string + Summary string + UpdatedAt time.Time +} + +type SessionsTool struct { + listFn func(limit int) []SessionInfo + historyFn func(key string, limit int) []providers.Message +} + +func NewSessionsTool(listFn func(limit int) []SessionInfo, historyFn func(key string, limit int) []providers.Message) *SessionsTool { + return &SessionsTool{listFn: listFn, historyFn: historyFn} +} + +func (t *SessionsTool) Name() string { return "sessions" } + +func (t *SessionsTool) Description() string { + return "Inspect sessions in current runtime: list or history" +} + +func (t *SessionsTool) Parameters() map[string]interface{} { + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "action": map[string]interface{}{"type": "string", "description": "list|history"}, + "key": map[string]interface{}{"type": "string", "description": "session key for history"}, + "limit": map[string]interface{}{"type": "integer", "description": "max items", "default": 20}, + }, + "required": []string{"action"}, + } +} + +func (t *SessionsTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { + _ = ctx + action, _ := args["action"].(string) + action = strings.ToLower(strings.TrimSpace(action)) + limit := 20 + if v, ok := args["limit"].(float64); ok && int(v) > 0 { + limit = int(v) + } + + switch action { + case "list": + if t.listFn == nil { + return "sessions list unavailable", nil + } + items := t.listFn(limit) + if len(items) == 0 { + return "No sessions.", nil + } + sort.Slice(items, func(i, j int) bool { return items[i].UpdatedAt.After(items[j].UpdatedAt) }) + var sb strings.Builder + sb.WriteString("Sessions:\n") + for _, s := range items { + sb.WriteString(fmt.Sprintf("- %s kind=%s updated=%s\n", s.Key, s.Kind, s.UpdatedAt.Format(time.RFC3339))) + } + return strings.TrimSpace(sb.String()), nil + case "history": + if t.historyFn == nil { + return "sessions history unavailable", nil + } + key, _ := args["key"].(string) + key = strings.TrimSpace(key) + if key == "" { + return "key is required for history", nil + } + h := t.historyFn(key, limit) + if len(h) == 0 { + return "No history.", nil + } + if len(h) > limit { + h = h[len(h)-limit:] + } + var sb strings.Builder + sb.WriteString(fmt.Sprintf("History for %s:\n", key)) + for _, m := range h { + content := strings.TrimSpace(m.Content) + if len(content) > 180 { + content = content[:180] + "..." + } + sb.WriteString(fmt.Sprintf("- [%s] %s\n", m.Role, content)) + } + return strings.TrimSpace(sb.String()), nil + default: + return "unsupported action", nil + } +} diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index 6ecb975..f29c81e 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -3,6 +3,7 @@ package tools import ( "context" "fmt" + "strings" "sync" "time" @@ -22,6 +23,7 @@ type SubagentTask struct { OriginChatID string Status string Result string + Steering []string Created int64 Updated int64 } @@ -230,3 +232,19 @@ func (sm *SubagentManager) KillTask(taskID string) bool { } return true } + +func (sm *SubagentManager) SteerTask(taskID, message string) bool { + sm.mu.Lock() + defer sm.mu.Unlock() + t, ok := sm.tasks[taskID] + if !ok { + return false + } + message = strings.TrimSpace(message) + if message == "" { + return false + } + t.Steering = append(t.Steering, message) + t.Updated = time.Now().UnixMilli() + return true +} diff --git a/pkg/tools/subagents_tool.go b/pkg/tools/subagents_tool.go index 1785bfb..b18debc 100644 --- a/pkg/tools/subagents_tool.go +++ b/pkg/tools/subagents_tool.go @@ -17,15 +17,16 @@ func NewSubagentsTool(m *SubagentManager) *SubagentsTool { func (t *SubagentsTool) Name() string { return "subagents" } func (t *SubagentsTool) Description() string { - return "Manage subagent runs in current process: list, info, kill" + return "Manage subagent runs in current process: list, info, kill, steer" } func (t *SubagentsTool) Parameters() map[string]interface{} { return map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "action": map[string]interface{}{"type": "string", "description": "list|info|kill"}, - "id": map[string]interface{}{"type": "string", "description": "subagent id for info/kill"}, + "action": map[string]interface{}{"type": "string", "description": "list|info|kill|steer"}, + "id": map[string]interface{}{"type": "string", "description": "subagent id for info/kill/steer"}, + "message": map[string]interface{}{"type": "string", "description": "steering message for steer action"}, }, "required": []string{"action"}, } @@ -40,6 +41,8 @@ func (t *SubagentsTool) Execute(ctx context.Context, args map[string]interface{} action = strings.ToLower(strings.TrimSpace(action)) id, _ := args["id"].(string) id = strings.TrimSpace(id) + message, _ := args["message"].(string) + message = strings.TrimSpace(message) switch action { case "list": @@ -70,6 +73,14 @@ func (t *SubagentsTool) Execute(ctx context.Context, args map[string]interface{} return "subagent not found", nil } return "subagent kill requested", nil + case "steer": + if id == "" || message == "" { + return "id and message are required for steer", nil + } + if !t.manager.SteerTask(id, message) { + return "subagent not found", nil + } + return "steering message accepted", nil default: return "unsupported action", nil }