mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-02 09:47:28 +08:00
Refactor runtime around world core
This commit is contained in:
@@ -3,39 +3,26 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/YspCoder/clawgo/pkg/config"
|
||||
"github.com/YspCoder/clawgo/pkg/runtimecfg"
|
||||
"github.com/YspCoder/clawgo/pkg/tools"
|
||||
)
|
||||
|
||||
var subagentRuntimeActionAliases = map[string]string{
|
||||
"info": "get",
|
||||
"create": "spawn",
|
||||
"trace": "thread",
|
||||
var runtimeAdminActionAliases = map[string]string{
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
if al.subagentRouter == nil {
|
||||
return nil, fmt.Errorf("subagent router is not configured")
|
||||
func (al *AgentLoop) HandleRuntimeAdmin(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) {
|
||||
if al == nil || al.agentManager == nil {
|
||||
return nil, fmt.Errorf("runtime admin is not configured")
|
||||
}
|
||||
action = strings.ToLower(strings.TrimSpace(action))
|
||||
if action == "" {
|
||||
action = "list"
|
||||
action = "snapshot"
|
||||
}
|
||||
if canonical := subagentRuntimeActionAliases[action]; canonical != "" {
|
||||
if canonical := runtimeAdminActionAliases[action]; canonical != "" {
|
||||
action = canonical
|
||||
}
|
||||
handler := al.subagentRuntimeHandlers()[action]
|
||||
handler := al.runtimeAdminHandlers()[action]
|
||||
if handler == nil {
|
||||
return nil, fmt.Errorf("unsupported action: %s", action)
|
||||
}
|
||||
@@ -44,566 +31,140 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
|
||||
|
||||
type runtimeAdminHandler func(context.Context, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func (al *AgentLoop) subagentRuntimeHandlers() map[string]runtimeAdminHandler {
|
||||
sm := al.subagentManager
|
||||
router := al.subagentRouter
|
||||
func (al *AgentLoop) runtimeAdminHandlers() map[string]runtimeAdminHandler {
|
||||
sm := al.agentManager
|
||||
return map[string]runtimeAdminHandler{
|
||||
"list": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
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
|
||||
},
|
||||
"snapshot": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
limit := runtimeIntArg(args, "limit", 100)
|
||||
return map[string]interface{}{"snapshot": sm.RuntimeSnapshot(limit)}, nil
|
||||
},
|
||||
"get": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
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
|
||||
},
|
||||
"spawn": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
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"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"message": msg}, nil
|
||||
},
|
||||
"dispatch_and_wait": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
taskInput := runtimeStringArg(args, "task")
|
||||
if taskInput == "" {
|
||||
return nil, fmt.Errorf("task is required")
|
||||
}
|
||||
task, err := router.DispatchTask(ctx, tools.RouterDispatchRequest{
|
||||
Task: taskInput,
|
||||
Label: runtimeStringArg(args, "label"),
|
||||
Role: runtimeStringArg(args, "role"),
|
||||
AgentID: runtimeStringArg(args, "agent_id"),
|
||||
NotifyMainPolicy: "internal_only",
|
||||
ThreadID: runtimeStringArg(args, "thread_id"),
|
||||
CorrelationID: runtimeStringArg(args, "correlation_id"),
|
||||
ParentRunID: runtimeStringArg(args, "parent_run_id"),
|
||||
OriginChannel: fallbackString(runtimeStringArg(args, "channel"), "webui"),
|
||||
OriginChatID: fallbackString(runtimeStringArg(args, "chat_id"), "webui"),
|
||||
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),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
waitTimeoutSec := runtimeIntArg(args, "wait_timeout_sec", 120)
|
||||
waitCtx := ctx
|
||||
var cancel context.CancelFunc
|
||||
if waitTimeoutSec > 0 {
|
||||
waitCtx, cancel = context.WithTimeout(ctx, time.Duration(waitTimeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
reply, err := router.WaitReply(waitCtx, task.ID, 100*time.Millisecond)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"task": cloneSubagentTask(task),
|
||||
"reply": reply,
|
||||
"merged": router.MergeResults([]*tools.RouterReply{reply}),
|
||||
}, nil
|
||||
},
|
||||
"registry": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
cfg := runtimecfg.Get()
|
||||
items := make([]map[string]interface{}, 0)
|
||||
if cfg != nil {
|
||||
items = make([]map[string]interface{}, 0, len(cfg.Agents.Subagents))
|
||||
for agentID, subcfg := range cfg.Agents.Subagents {
|
||||
promptFileFound := false
|
||||
if strings.TrimSpace(subcfg.SystemPromptFile) != "" {
|
||||
if absPath, err := al.resolvePromptFilePath(subcfg.SystemPromptFile); err == nil {
|
||||
if info, statErr := os.Stat(absPath); statErr == nil && !info.IsDir() {
|
||||
promptFileFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
toolInfo := al.describeSubagentTools(subcfg.Tools.Allowlist)
|
||||
items = append(items, map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
"enabled": subcfg.Enabled,
|
||||
"type": subcfg.Type,
|
||||
"transport": fallbackString(strings.TrimSpace(subcfg.Transport), "local"),
|
||||
"node_id": strings.TrimSpace(subcfg.NodeID),
|
||||
"parent_agent_id": strings.TrimSpace(subcfg.ParentAgentID),
|
||||
"notify_main_policy": fallbackString(strings.TrimSpace(subcfg.NotifyMainPolicy), "final_only"),
|
||||
"display_name": subcfg.DisplayName,
|
||||
"role": subcfg.Role,
|
||||
"description": subcfg.Description,
|
||||
"system_prompt_file": subcfg.SystemPromptFile,
|
||||
"prompt_file_found": promptFileFound,
|
||||
"memory_namespace": subcfg.MemoryNamespace,
|
||||
"tool_allowlist": append([]string(nil), subcfg.Tools.Allowlist...),
|
||||
"tool_visibility": toolInfo,
|
||||
"effective_tools": toolInfo["effective_tools"],
|
||||
"inherited_tools": toolInfo["inherited_tools"],
|
||||
"routing_keywords": routeKeywordsForRegistry(cfg.Agents.Router.Rules, agentID),
|
||||
"managed_by": "config.json",
|
||||
})
|
||||
snapshot := sm.RuntimeSnapshot(limit)
|
||||
if al.worldRuntime != nil && al.worldRuntime.Enabled() {
|
||||
if worldSnap, err := al.worldRuntime.Snapshot(limit); err == nil {
|
||||
snapshot.World = worldSnap
|
||||
}
|
||||
}
|
||||
if store := sm.ProfileStore(); store != nil {
|
||||
if profiles, err := store.List(); err == nil {
|
||||
for _, profile := range profiles {
|
||||
if strings.TrimSpace(profile.ManagedBy) != "node_registry" {
|
||||
continue
|
||||
}
|
||||
toolInfo := al.describeSubagentTools(profile.ToolAllowlist)
|
||||
items = append(items, map[string]interface{}{
|
||||
"agent_id": profile.AgentID,
|
||||
"enabled": strings.EqualFold(strings.TrimSpace(profile.Status), "active"),
|
||||
"type": "node_branch",
|
||||
"transport": profile.Transport,
|
||||
"node_id": profile.NodeID,
|
||||
"parent_agent_id": profile.ParentAgentID,
|
||||
"notify_main_policy": fallbackString(strings.TrimSpace(profile.NotifyMainPolicy), "final_only"),
|
||||
"display_name": profile.Name,
|
||||
"role": profile.Role,
|
||||
"description": "Node-registered remote main agent branch",
|
||||
"system_prompt_file": profile.SystemPromptFile,
|
||||
"prompt_file_found": false,
|
||||
"memory_namespace": profile.MemoryNamespace,
|
||||
"tool_allowlist": append([]string(nil), profile.ToolAllowlist...),
|
||||
"tool_visibility": toolInfo,
|
||||
"effective_tools": toolInfo["effective_tools"],
|
||||
"inherited_tools": toolInfo["inherited_tools"],
|
||||
"routing_keywords": []string{},
|
||||
"managed_by": profile.ManagedBy,
|
||||
})
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{"snapshot": snapshot}, nil
|
||||
},
|
||||
"world_snapshot": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
out, err := al.worldRuntime.Snapshot(runtimeIntArg(args, "limit", 20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"snapshot": out}, nil
|
||||
},
|
||||
"world_tick": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
out, err := al.worldRuntime.Tick(ctx, fallbackString(runtimeStringArg(args, "source"), "runtime_admin"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"message": out}, nil
|
||||
},
|
||||
"world_npc_list": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
items, err := al.worldRuntime.NPCList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
left, _ := items[i]["agent_id"].(string)
|
||||
right, _ := items[j]["agent_id"].(string)
|
||||
return left < right
|
||||
})
|
||||
return map[string]interface{}{"items": items}, nil
|
||||
},
|
||||
"set_config_subagent_enabled": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
agentID := runtimeStringArg(args, "agent_id")
|
||||
if agentID == "" {
|
||||
return nil, fmt.Errorf("agent_id is required")
|
||||
"world_npc_get": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
if al.isProtectedMainAgent(agentID) {
|
||||
return nil, fmt.Errorf("main agent %q cannot be disabled", agentID)
|
||||
}
|
||||
enabled, ok := runtimeBoolArg(args, "enabled")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("enabled is required")
|
||||
}
|
||||
return tools.UpsertConfigSubagent(al.configPath, map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
"enabled": enabled,
|
||||
})
|
||||
},
|
||||
"delete_config_subagent": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
agentID := runtimeStringArg(args, "agent_id")
|
||||
if agentID == "" {
|
||||
return nil, fmt.Errorf("agent_id is required")
|
||||
}
|
||||
if al.isProtectedMainAgent(agentID) {
|
||||
return nil, fmt.Errorf("main agent %q cannot be deleted", agentID)
|
||||
}
|
||||
return tools.DeleteConfigSubagent(al.configPath, agentID)
|
||||
},
|
||||
"upsert_config_subagent": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
return tools.UpsertConfigSubagent(al.configPath, args)
|
||||
},
|
||||
"prompt_file_get": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
relPath := runtimeStringArg(args, "path")
|
||||
if relPath == "" {
|
||||
return nil, fmt.Errorf("path is required")
|
||||
}
|
||||
absPath, err := al.resolvePromptFilePath(relPath)
|
||||
item, found, err := al.worldRuntime.NPCGet(runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return map[string]interface{}{"found": false, "path": relPath, "content": ""}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"found": true, "path": relPath, "content": string(data)}, nil
|
||||
return map[string]interface{}{"found": found, "item": item}, nil
|
||||
},
|
||||
"prompt_file_set": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
relPath := runtimeStringArg(args, "path")
|
||||
if relPath == "" {
|
||||
return nil, fmt.Errorf("path is required")
|
||||
"world_entity_list": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
content := runtimeRawStringArg(args, "content")
|
||||
absPath, err := al.resolvePromptFilePath(relPath)
|
||||
items, err := al.worldRuntime.EntityList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "path": relPath, "bytes": len(content)}, nil
|
||||
return map[string]interface{}{"items": items}, nil
|
||||
},
|
||||
"prompt_file_bootstrap": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
agentID := runtimeStringArg(args, "agent_id")
|
||||
if agentID == "" {
|
||||
return nil, fmt.Errorf("agent_id is required")
|
||||
"world_entity_get": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
relPath := runtimeStringArg(args, "path")
|
||||
if relPath == "" {
|
||||
relPath = filepath.ToSlash(filepath.Join("agents", agentID, "AGENT.md"))
|
||||
}
|
||||
absPath, err := al.resolvePromptFilePath(relPath)
|
||||
item, found, err := al.worldRuntime.EntityGet(runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overwrite, _ := args["overwrite"].(bool)
|
||||
if _, err := os.Stat(absPath); err == nil && !overwrite {
|
||||
data, readErr := os.ReadFile(absPath)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"ok": true,
|
||||
"created": false,
|
||||
"path": relPath,
|
||||
"content": string(data),
|
||||
}, nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content := buildPromptTemplate(agentID, runtimeStringArg(args, "role"), runtimeStringArg(args, "display_name"))
|
||||
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"ok": true,
|
||||
"created": true,
|
||||
"path": relPath,
|
||||
"content": content,
|
||||
}, nil
|
||||
return map[string]interface{}{"found": found, "item": item}, nil
|
||||
},
|
||||
"kill": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
"world_get": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
item, err := al.worldRuntime.WorldGet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok := sm.KillTask(taskID)
|
||||
return map[string]interface{}{"ok": ok}, nil
|
||||
return item, nil
|
||||
},
|
||||
"resume": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
"world_event_log": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
items, err := al.worldRuntime.EventLog(runtimeIntArg(args, "limit", 20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
label, ok := sm.ResumeTask(ctx, taskID)
|
||||
return map[string]interface{}{"ok": ok, "label": label}, nil
|
||||
return map[string]interface{}{"items": items}, nil
|
||||
},
|
||||
"steer": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
"world_npc_create": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
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
|
||||
return al.worldRuntime.CreateNPC(ctx, args)
|
||||
},
|
||||
"send": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
"world_entity_create": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
msg := runtimeStringArg(args, "message")
|
||||
if msg == "" {
|
||||
return nil, fmt.Errorf("message is required")
|
||||
}
|
||||
ok := sm.SendTaskMessage(taskID, msg)
|
||||
return map[string]interface{}{"ok": ok}, nil
|
||||
return al.worldRuntime.CreateEntity(ctx, args)
|
||||
},
|
||||
"reply": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
"world_quest_list": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
items, err := al.worldRuntime.QuestList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := runtimeStringArg(args, "message")
|
||||
if msg == "" {
|
||||
return nil, fmt.Errorf("message is required")
|
||||
}
|
||||
ok := sm.ReplyToTask(taskID, runtimeStringArg(args, "message_id"), msg)
|
||||
return map[string]interface{}{"ok": ok}, nil
|
||||
return map[string]interface{}{"items": items}, nil
|
||||
},
|
||||
"ack": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
taskID, err := resolveSubagentTaskIDForRuntime(sm, runtimeStringArg(args, "id"))
|
||||
"world_quest_get": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
item, found, err := al.worldRuntime.QuestGet(runtimeStringArg(args, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messageID := runtimeStringArg(args, "message_id")
|
||||
if messageID == "" {
|
||||
return nil, fmt.Errorf("message_id is required")
|
||||
}
|
||||
ok := sm.AckTaskMessage(taskID, messageID)
|
||||
return map[string]interface{}{"ok": ok}, nil
|
||||
return map[string]interface{}{"found": found, "item": item}, nil
|
||||
},
|
||||
"thread": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
threadID := runtimeStringArg(args, "thread_id")
|
||||
if threadID == "" {
|
||||
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
|
||||
}
|
||||
threadID = strings.TrimSpace(task.ThreadID)
|
||||
"world_quest_create": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
if al.worldRuntime == nil {
|
||||
return nil, fmt.Errorf("world runtime is not configured")
|
||||
}
|
||||
if threadID == "" {
|
||||
return nil, fmt.Errorf("thread_id is required")
|
||||
}
|
||||
thread, ok := sm.Thread(threadID)
|
||||
if !ok {
|
||||
return map[string]interface{}{"found": false}, nil
|
||||
}
|
||||
items, err := sm.ThreadMessages(threadID, runtimeIntArg(args, "limit", 50))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"found": true, "thread": thread, "messages": items}, nil
|
||||
},
|
||||
"stream": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
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
|
||||
}
|
||||
events, err := sm.Events(taskID, runtimeIntArg(args, "limit", 100))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var thread *tools.AgentThread
|
||||
var messages []tools.AgentMessage
|
||||
if strings.TrimSpace(task.ThreadID) != "" {
|
||||
if th, ok := sm.Thread(task.ThreadID); ok {
|
||||
thread = th
|
||||
}
|
||||
messages, err = sm.ThreadMessages(task.ThreadID, runtimeIntArg(args, "limit", 100))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
stream := mergeSubagentStream(events, messages)
|
||||
return map[string]interface{}{
|
||||
"found": true,
|
||||
"task": cloneSubagentTask(task),
|
||||
"thread": thread,
|
||||
"items": stream,
|
||||
}, nil
|
||||
},
|
||||
"inbox": func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||||
agentID := runtimeStringArg(args, "agent_id")
|
||||
if agentID == "" {
|
||||
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
|
||||
}
|
||||
agentID = strings.TrimSpace(task.AgentID)
|
||||
}
|
||||
if agentID == "" {
|
||||
return nil, fmt.Errorf("agent_id is required")
|
||||
}
|
||||
items, err := sm.Inbox(agentID, runtimeIntArg(args, "limit", 50))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"found": true, "agent_id": agentID, "messages": items}, nil
|
||||
return al.worldRuntime.CreateQuest(ctx, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) describeSubagentTools(allowlist []string) map[string]interface{} {
|
||||
inherited := implicitSubagentTools()
|
||||
allTools := make([]string, 0)
|
||||
if al != nil && al.tools != nil {
|
||||
allTools = al.tools.List()
|
||||
sort.Strings(allTools)
|
||||
}
|
||||
|
||||
normalizedAllow := normalizeToolAllowlist(allowlist)
|
||||
mode := "allowlist"
|
||||
effective := make([]string, 0)
|
||||
if len(normalizedAllow) == 0 {
|
||||
mode = "unrestricted"
|
||||
effective = append(effective, allTools...)
|
||||
} else if _, ok := normalizedAllow["*"]; ok {
|
||||
mode = "all"
|
||||
effective = append(effective, allTools...)
|
||||
} else if _, ok := normalizedAllow["all"]; ok {
|
||||
mode = "all"
|
||||
effective = append(effective, allTools...)
|
||||
} else {
|
||||
for _, name := range allTools {
|
||||
if isToolNameAllowed(normalizedAllow, name) || isImplicitlyAllowedSubagentTool(name) {
|
||||
effective = append(effective, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"mode": mode,
|
||||
"raw_allowlist": append([]string(nil), allowlist...),
|
||||
"inherited_tools": inherited,
|
||||
"inherited_tool_count": len(inherited),
|
||||
"effective_tools": effective,
|
||||
"effective_tool_count": len(effective),
|
||||
}
|
||||
}
|
||||
|
||||
func implicitSubagentTools() []string {
|
||||
out := make([]string, 0, 1)
|
||||
if isImplicitlyAllowedSubagentTool("skill_exec") {
|
||||
out = append(out, "skill_exec")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeSubagentStream(events []tools.SubagentRunEvent, messages []tools.AgentMessage) []map[string]interface{} {
|
||||
items := make([]map[string]interface{}, 0, len(events)+len(messages))
|
||||
for _, evt := range events {
|
||||
items = append(items, map[string]interface{}{
|
||||
"kind": "event",
|
||||
"at": evt.At,
|
||||
"run_id": evt.RunID,
|
||||
"agent_id": evt.AgentID,
|
||||
"event_type": evt.Type,
|
||||
"status": evt.Status,
|
||||
"message": evt.Message,
|
||||
"retry_count": evt.RetryCount,
|
||||
})
|
||||
}
|
||||
for _, msg := range messages {
|
||||
items = append(items, map[string]interface{}{
|
||||
"kind": "message",
|
||||
"at": msg.CreatedAt,
|
||||
"message_id": msg.MessageID,
|
||||
"thread_id": msg.ThreadID,
|
||||
"from_agent": msg.FromAgent,
|
||||
"to_agent": msg.ToAgent,
|
||||
"reply_to": msg.ReplyTo,
|
||||
"correlation_id": msg.CorrelationID,
|
||||
"message_type": msg.Type,
|
||||
"content": msg.Content,
|
||||
"status": msg.Status,
|
||||
"requires_reply": msg.RequiresReply,
|
||||
})
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
left, _ := items[i]["at"].(int64)
|
||||
right, _ := items[j]["at"].(int64)
|
||||
if left != right {
|
||||
return left < right
|
||||
}
|
||||
return fmt.Sprintf("%v", items[i]["kind"]) < fmt.Sprintf("%v", items[j]["kind"])
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
func firstNonEmptyString(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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 runtimeStringArg(args map[string]interface{}, key string) string {
|
||||
return tools.MapStringArg(args, key)
|
||||
}
|
||||
@@ -626,84 +187,3 @@ func fallbackString(v, fallback string) string {
|
||||
}
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
func routeKeywordsForRegistry(rules []config.AgentRouteRule, agentID string) []string {
|
||||
agentID = strings.TrimSpace(agentID)
|
||||
for _, rule := range rules {
|
||||
if strings.TrimSpace(rule.AgentID) == agentID {
|
||||
return append([]string(nil), rule.Keywords...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (al *AgentLoop) isProtectedMainAgent(agentID string) bool {
|
||||
agentID = strings.TrimSpace(agentID)
|
||||
if agentID == "" {
|
||||
return false
|
||||
}
|
||||
cfg := runtimecfg.Get()
|
||||
if cfg == nil {
|
||||
return agentID == "main"
|
||||
}
|
||||
mainID := strings.TrimSpace(cfg.Agents.Router.MainAgentID)
|
||||
if mainID == "" {
|
||||
mainID = "main"
|
||||
}
|
||||
return agentID == mainID
|
||||
}
|
||||
|
||||
func (al *AgentLoop) resolvePromptFilePath(relPath string) (string, error) {
|
||||
relPath = strings.TrimSpace(relPath)
|
||||
if relPath == "" {
|
||||
return "", fmt.Errorf("path is required")
|
||||
}
|
||||
if filepath.IsAbs(relPath) {
|
||||
return "", fmt.Errorf("path must be relative")
|
||||
}
|
||||
cleaned := filepath.Clean(relPath)
|
||||
if cleaned == "." || strings.HasPrefix(cleaned, "..") {
|
||||
return "", fmt.Errorf("path must stay within workspace")
|
||||
}
|
||||
workspace := "."
|
||||
if al != nil && strings.TrimSpace(al.workspace) != "" {
|
||||
workspace = al.workspace
|
||||
}
|
||||
return filepath.Join(workspace, cleaned), nil
|
||||
}
|
||||
|
||||
func buildPromptTemplate(agentID, role, displayName string) string {
|
||||
agentID = strings.TrimSpace(agentID)
|
||||
role = strings.TrimSpace(role)
|
||||
displayName = strings.TrimSpace(displayName)
|
||||
title := displayName
|
||||
if title == "" {
|
||||
title = agentID
|
||||
}
|
||||
if title == "" {
|
||||
title = "subagent"
|
||||
}
|
||||
if role == "" {
|
||||
role = "worker"
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`# %s
|
||||
|
||||
## Role
|
||||
You are the %s subagent. Work within your role boundary and report concrete outcomes.
|
||||
|
||||
## Priorities
|
||||
- Follow workspace-level policy from workspace/AGENTS.md.
|
||||
- Complete the assigned task directly. Do not redefine the objective.
|
||||
- Prefer concrete edits, verification, and concise reporting over long analysis.
|
||||
|
||||
## Collaboration
|
||||
- Treat the main agent as the coordinator unless the task explicitly says otherwise.
|
||||
- Surface blockers, assumptions, and verification status in your reply.
|
||||
- Keep outputs short and execution-focused.
|
||||
|
||||
## Output Format
|
||||
- Summary: what you changed or checked.
|
||||
- Risks: anything not verified or still uncertain.
|
||||
- Next: the most useful immediate follow-up, if any.
|
||||
`, title, role))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user