mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-15 06:17:28 +08:00
feat: ship subagent runtime and remove autonomy/task legacy
This commit is contained in:
@@ -59,6 +59,8 @@ type AgentLoop struct {
|
||||
sessionProvider map[string]string
|
||||
streamMu sync.Mutex
|
||||
sessionStreamed map[string]bool
|
||||
subagentManager *tools.SubagentManager
|
||||
orchestrator *tools.Orchestrator
|
||||
}
|
||||
|
||||
// StartupCompactionReport provides startup memory/session maintenance stats.
|
||||
@@ -236,6 +238,8 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
sessionStreamed: map[string]bool{},
|
||||
providerResponses: map[string]config.ProviderResponsesConfig{},
|
||||
telegramStreaming: cfg.Channels.Telegram.Streaming,
|
||||
subagentManager: subagentManager,
|
||||
orchestrator: orchestrator,
|
||||
}
|
||||
// Initialize provider fallback chain (primary + proxy_fallbacks).
|
||||
loop.providerPool = map[string]providers.LLMProvider{}
|
||||
@@ -479,14 +483,6 @@ func buildAuditTaskID(msg bus.InboundMessage) string {
|
||||
sessionPart = "default"
|
||||
}
|
||||
return "heartbeat:" + sessionPart
|
||||
case "autonomy":
|
||||
norm := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(msg.Content, "\n", " ")))
|
||||
if len(norm) > 180 {
|
||||
norm = norm[:180]
|
||||
}
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(msg.SessionKey + "|" + norm))
|
||||
return fmt.Sprintf("autonomy:%08x", h.Sum32())
|
||||
default:
|
||||
return fmt.Sprintf("%s-%d", sessionPart, time.Now().Unix()%100000)
|
||||
}
|
||||
@@ -524,7 +520,7 @@ func (al *AgentLoop) appendTaskAuditEvent(taskID string, msg bus.InboundMessage,
|
||||
"sender_id": msg.SenderID,
|
||||
"status": status,
|
||||
"source": source,
|
||||
"idle_run": source == "autonomy",
|
||||
"idle_run": false,
|
||||
"duration_ms": durationMs,
|
||||
"suppressed": suppressed,
|
||||
"retry_count": 0,
|
||||
@@ -1719,11 +1715,12 @@ func validateParallelAllowlistArgs(allow map[string]struct{}, args map[string]in
|
||||
}
|
||||
|
||||
func normalizeToolAllowlist(in []string) map[string]struct{} {
|
||||
if len(in) == 0 {
|
||||
expanded := tools.ExpandToolAllowlistEntries(in)
|
||||
if len(expanded) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]struct{}, len(in))
|
||||
for _, item := range in {
|
||||
out := make(map[string]struct{}, len(expanded))
|
||||
for _, item := range expanded {
|
||||
name := strings.ToLower(strings.TrimSpace(item))
|
||||
if name == "" {
|
||||
continue
|
||||
|
||||
@@ -42,3 +42,23 @@ func TestEnsureToolAllowedByContextParallelNested(t *testing.T) {
|
||||
t.Fatalf("expected parallel with disallowed nested tool to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureToolAllowedByContext_GroupAllowlist(t *testing.T) {
|
||||
ctx := withToolAllowlistContext(context.Background(), []string{"group:files_read"})
|
||||
if err := ensureToolAllowedByContext(ctx, "read_file", map[string]interface{}{}); err != nil {
|
||||
t.Fatalf("expected files_read group to allow read_file, got: %v", err)
|
||||
}
|
||||
if err := ensureToolAllowedByContext(ctx, "write_file", map[string]interface{}{}); err == nil {
|
||||
t.Fatalf("expected files_read group to block write_file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureToolAllowedByContext_GroupAliasToken(t *testing.T) {
|
||||
ctx := withToolAllowlistContext(context.Background(), []string{"@pipeline"})
|
||||
if err := ensureToolAllowedByContext(ctx, "pipeline_status", map[string]interface{}{}); err != nil {
|
||||
t.Fatalf("expected @pipeline to allow pipeline_status, got: %v", err)
|
||||
}
|
||||
if err := ensureToolAllowedByContext(ctx, "memory_search", map[string]interface{}{}); err == nil {
|
||||
t.Fatalf("expected @pipeline to block memory_search")
|
||||
}
|
||||
}
|
||||
|
||||
296
pkg/agent/runtime_admin.go
Normal file
296
pkg/agent/runtime_admin.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"clawgo/pkg/tools"
|
||||
)
|
||||
|
||||
func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) {
|
||||
if al == nil || al.subagentManager == nil {
|
||||
return nil, fmt.Errorf("subagent runtime is not configured")
|
||||
}
|
||||
action = strings.ToLower(strings.TrimSpace(action))
|
||||
if action == "" {
|
||||
action = "list"
|
||||
}
|
||||
|
||||
sm := al.subagentManager
|
||||
switch action {
|
||||
case "list":
|
||||
tasks := sm.ListTasks()
|
||||
items := make([]*tools.SubagentTask, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
items = append(items, cloneSubagentTask(task))
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].Created > items[j].Created })
|
||||
return map[string]interface{}{"items": items}, nil
|
||||
case "get", "info":
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task, ok := sm.GetTask(taskID)
|
||||
if !ok {
|
||||
return map[string]interface{}{"found": false}, nil
|
||||
}
|
||||
return map[string]interface{}{"found": true, "task": cloneSubagentTask(task)}, nil
|
||||
case "spawn", "create":
|
||||
taskInput := runtimeStringArg(args, "task")
|
||||
if taskInput == "" {
|
||||
return nil, fmt.Errorf("task is required")
|
||||
}
|
||||
msg, err := sm.Spawn(ctx, tools.SubagentSpawnOptions{
|
||||
Task: taskInput,
|
||||
Label: runtimeStringArg(args, "label"),
|
||||
Role: runtimeStringArg(args, "role"),
|
||||
AgentID: runtimeStringArg(args, "agent_id"),
|
||||
MaxRetries: runtimeIntArg(args, "max_retries", 0),
|
||||
RetryBackoff: runtimeIntArg(args, "retry_backoff_ms", 0),
|
||||
TimeoutSec: runtimeIntArg(args, "timeout_sec", 0),
|
||||
MaxTaskChars: runtimeIntArg(args, "max_task_chars", 0),
|
||||
MaxResultChars: runtimeIntArg(args, "max_result_chars", 0),
|
||||
OriginChannel: fallbackString(runtimeStringArg(args, "channel"), "webui"),
|
||||
OriginChatID: fallbackString(runtimeStringArg(args, "chat_id"), "webui"),
|
||||
PipelineID: runtimeStringArg(args, "pipeline_id"),
|
||||
PipelineTask: runtimeStringArg(args, "task_id"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"message": msg}, nil
|
||||
case "kill":
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok := sm.KillTask(taskID)
|
||||
return map[string]interface{}{"ok": ok}, nil
|
||||
case "resume":
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
label, ok := sm.ResumeTask(ctx, taskID)
|
||||
return map[string]interface{}{"ok": ok, "label": label}, nil
|
||||
case "steer", "send":
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := runtimeStringArg(args, "message")
|
||||
if msg == "" {
|
||||
return nil, fmt.Errorf("message is required")
|
||||
}
|
||||
ok := sm.SteerTask(taskID, msg)
|
||||
return map[string]interface{}{"ok": ok}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported action: %s", action)
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) HandlePipelineRuntime(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) {
|
||||
if al == nil || al.orchestrator == nil {
|
||||
return nil, fmt.Errorf("pipeline runtime is not configured")
|
||||
}
|
||||
action = strings.ToLower(strings.TrimSpace(action))
|
||||
if action == "" {
|
||||
action = "list"
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "list":
|
||||
return map[string]interface{}{"items": al.orchestrator.ListPipelines()}, nil
|
||||
case "get", "status":
|
||||
pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id"))
|
||||
if strings.TrimSpace(pipelineID) == "" {
|
||||
return nil, fmt.Errorf("pipeline_id is required")
|
||||
}
|
||||
p, ok := al.orchestrator.GetPipeline(strings.TrimSpace(pipelineID))
|
||||
if !ok {
|
||||
return map[string]interface{}{"found": false}, nil
|
||||
}
|
||||
return map[string]interface{}{"found": true, "pipeline": p}, nil
|
||||
case "ready":
|
||||
pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id"))
|
||||
if strings.TrimSpace(pipelineID) == "" {
|
||||
return nil, fmt.Errorf("pipeline_id is required")
|
||||
}
|
||||
items, err := al.orchestrator.ReadyTasks(strings.TrimSpace(pipelineID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"items": items}, nil
|
||||
case "create":
|
||||
objective := runtimeStringArg(args, "objective")
|
||||
if objective == "" {
|
||||
return nil, fmt.Errorf("objective is required")
|
||||
}
|
||||
specs, err := parsePipelineSpecsForRuntime(args["tasks"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
label := runtimeStringArg(args, "label")
|
||||
p, err := al.orchestrator.CreatePipeline(label, objective, "webui", "webui", specs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"pipeline": p}, nil
|
||||
case "state_set":
|
||||
pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id"))
|
||||
key := runtimeStringArg(args, "key")
|
||||
if strings.TrimSpace(pipelineID) == "" || strings.TrimSpace(key) == "" {
|
||||
return nil, fmt.Errorf("pipeline_id and key are required")
|
||||
}
|
||||
value, ok := args["value"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("value is required")
|
||||
}
|
||||
if err := al.orchestrator.SetSharedState(strings.TrimSpace(pipelineID), strings.TrimSpace(key), value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, _ := al.orchestrator.GetPipeline(strings.TrimSpace(pipelineID))
|
||||
return map[string]interface{}{"ok": true, "pipeline": p}, nil
|
||||
case "dispatch":
|
||||
pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id"))
|
||||
if strings.TrimSpace(pipelineID) == "" {
|
||||
return nil, fmt.Errorf("pipeline_id is required")
|
||||
}
|
||||
maxDispatch := runtimeIntArg(args, "max_dispatch", 3)
|
||||
dispatchTool := tools.NewPipelineDispatchTool(al.orchestrator, al.subagentManager)
|
||||
result, err := dispatchTool.Execute(ctx, map[string]interface{}{
|
||||
"pipeline_id": strings.TrimSpace(pipelineID),
|
||||
"max_dispatch": float64(maxDispatch),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, _ := al.orchestrator.GetPipeline(strings.TrimSpace(pipelineID))
|
||||
return map[string]interface{}{"message": result, "pipeline": p}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported action: %s", action)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneSubagentTask(in *tools.SubagentTask) *tools.SubagentTask {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := *in
|
||||
if len(in.ToolAllowlist) > 0 {
|
||||
out.ToolAllowlist = append([]string(nil), in.ToolAllowlist...)
|
||||
}
|
||||
if len(in.Steering) > 0 {
|
||||
out.Steering = append([]string(nil), in.Steering...)
|
||||
}
|
||||
if in.SharedState != nil {
|
||||
out.SharedState = make(map[string]interface{}, len(in.SharedState))
|
||||
for k, v := range in.SharedState {
|
||||
out.SharedState[k] = v
|
||||
}
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
func resolveSubagentTaskIDForRuntime(sm *tools.SubagentManager, raw string) (string, error) {
|
||||
id := strings.TrimSpace(raw)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("id is required")
|
||||
}
|
||||
if !strings.HasPrefix(id, "#") {
|
||||
return id, nil
|
||||
}
|
||||
idx, err := strconv.Atoi(strings.TrimPrefix(id, "#"))
|
||||
if err != nil || idx <= 0 {
|
||||
return "", fmt.Errorf("invalid subagent index")
|
||||
}
|
||||
tasks := sm.ListTasks()
|
||||
if len(tasks) == 0 {
|
||||
return "", fmt.Errorf("no subagents")
|
||||
}
|
||||
sort.Slice(tasks, func(i, j int) bool { return tasks[i].Created > tasks[j].Created })
|
||||
if idx > len(tasks) {
|
||||
return "", fmt.Errorf("subagent index out of range")
|
||||
}
|
||||
return tasks[idx-1].ID, nil
|
||||
}
|
||||
|
||||
func parsePipelineSpecsForRuntime(raw interface{}) ([]tools.PipelineSpec, error) {
|
||||
items, ok := raw.([]interface{})
|
||||
if !ok || len(items) == 0 {
|
||||
return nil, fmt.Errorf("tasks is required")
|
||||
}
|
||||
specs := make([]tools.PipelineSpec, 0, len(items))
|
||||
for i, item := range items {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tasks[%d] must be object", i)
|
||||
}
|
||||
id := runtimeStringArg(m, "id")
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("tasks[%d].id is required", i)
|
||||
}
|
||||
goal := runtimeStringArg(m, "goal")
|
||||
if goal == "" {
|
||||
return nil, fmt.Errorf("tasks[%d].goal is required", i)
|
||||
}
|
||||
spec := tools.PipelineSpec{
|
||||
ID: id,
|
||||
Role: runtimeStringArg(m, "role"),
|
||||
Goal: goal,
|
||||
}
|
||||
if deps, ok := m["depends_on"].([]interface{}); ok {
|
||||
spec.DependsOn = make([]string, 0, len(deps))
|
||||
for _, dep := range deps {
|
||||
d, _ := dep.(string)
|
||||
d = strings.TrimSpace(d)
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
spec.DependsOn = append(spec.DependsOn, d)
|
||||
}
|
||||
}
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
func runtimeStringArg(args map[string]interface{}, key string) string {
|
||||
if args == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := args[key].(string)
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
func runtimeIntArg(args map[string]interface{}, key string, fallback int) int {
|
||||
if args == nil {
|
||||
return fallback
|
||||
}
|
||||
switch v := args[key].(type) {
|
||||
case int:
|
||||
if v > 0 {
|
||||
return v
|
||||
}
|
||||
case int64:
|
||||
if v > 0 {
|
||||
return int(v)
|
||||
}
|
||||
case float64:
|
||||
if v > 0 {
|
||||
return int(v)
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func fallbackString(v, fallback string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return fallback
|
||||
}
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
Reference in New Issue
Block a user