mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 01:27:28 +08:00
685 lines
21 KiB
Go
685 lines
21 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"clawgo/pkg/config"
|
|
"clawgo/pkg/runtimecfg"
|
|
"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")
|
|
}
|
|
if al.subagentRouter == nil {
|
|
return nil, fmt.Errorf("subagent router is not configured")
|
|
}
|
|
action = strings.ToLower(strings.TrimSpace(action))
|
|
if action == "" {
|
|
action = "list"
|
|
}
|
|
|
|
sm := al.subagentManager
|
|
router := al.subagentRouter
|
|
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 "dispatch_and_wait":
|
|
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"),
|
|
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
|
|
case "draft_config_subagent":
|
|
description := runtimeStringArg(args, "description")
|
|
if description == "" {
|
|
return nil, fmt.Errorf("description is required")
|
|
}
|
|
draft := tools.DraftConfigSubagent(description, runtimeStringArg(args, "agent_id_hint"))
|
|
return map[string]interface{}{"draft": draft}, nil
|
|
case "registry":
|
|
cfg := runtimecfg.Get()
|
|
if cfg == nil {
|
|
return map[string]interface{}{"items": []map[string]interface{}{}}, 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
|
|
}
|
|
}
|
|
}
|
|
items = append(items, map[string]interface{}{
|
|
"agent_id": agentID,
|
|
"enabled": subcfg.Enabled,
|
|
"type": subcfg.Type,
|
|
"display_name": subcfg.DisplayName,
|
|
"role": subcfg.Role,
|
|
"description": subcfg.Description,
|
|
"system_prompt": subcfg.SystemPrompt,
|
|
"system_prompt_file": subcfg.SystemPromptFile,
|
|
"prompt_file_found": promptFileFound,
|
|
"memory_namespace": subcfg.MemoryNamespace,
|
|
"tool_allowlist": append([]string(nil), subcfg.Tools.Allowlist...),
|
|
"routing_keywords": routeKeywordsForRegistry(cfg.Agents.Router.Rules, agentID),
|
|
})
|
|
}
|
|
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
|
|
case "pending_drafts":
|
|
items := make([]map[string]interface{}, 0, len(al.pendingSubagentDraft))
|
|
for sessionKey, draft := range al.pendingSubagentDraft {
|
|
items = append(items, map[string]interface{}{
|
|
"session_key": sessionKey,
|
|
"draft": cloneDraftMap(draft),
|
|
})
|
|
}
|
|
sort.Slice(items, func(i, j int) bool {
|
|
left, _ := items[i]["session_key"].(string)
|
|
right, _ := items[j]["session_key"].(string)
|
|
return left < right
|
|
})
|
|
return map[string]interface{}{"items": items}, nil
|
|
case "clear_pending_draft":
|
|
sessionKey := fallbackString(runtimeStringArg(args, "session_key"), "main")
|
|
if al.loadPendingSubagentDraft(sessionKey) == nil {
|
|
return map[string]interface{}{"ok": false, "found": false}, nil
|
|
}
|
|
al.deletePendingSubagentDraft(sessionKey)
|
|
return map[string]interface{}{"ok": true, "found": true, "session_key": sessionKey}, nil
|
|
case "confirm_pending_draft":
|
|
sessionKey := fallbackString(runtimeStringArg(args, "session_key"), "main")
|
|
msg, handled, err := al.confirmPendingSubagentDraft(sessionKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]interface{}{"ok": handled, "found": handled, "session_key": sessionKey, "message": msg}, nil
|
|
case "set_config_subagent_enabled":
|
|
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 disabled", agentID)
|
|
}
|
|
enabled, ok := args["enabled"].(bool)
|
|
if !ok {
|
|
return nil, fmt.Errorf("enabled is required")
|
|
}
|
|
return tools.UpsertConfigSubagent(al.configPath, map[string]interface{}{
|
|
"agent_id": agentID,
|
|
"enabled": enabled,
|
|
})
|
|
case "delete_config_subagent":
|
|
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)
|
|
case "upsert_config_subagent":
|
|
return tools.UpsertConfigSubagent(al.configPath, args)
|
|
case "prompt_file_get":
|
|
relPath := runtimeStringArg(args, "path")
|
|
if relPath == "" {
|
|
return nil, fmt.Errorf("path is required")
|
|
}
|
|
absPath, err := al.resolvePromptFilePath(relPath)
|
|
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
|
|
case "prompt_file_set":
|
|
relPath := runtimeStringArg(args, "path")
|
|
if relPath == "" {
|
|
return nil, fmt.Errorf("path is required")
|
|
}
|
|
content := runtimeRawStringArg(args, "content")
|
|
absPath, err := al.resolvePromptFilePath(relPath)
|
|
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
|
|
case "prompt_file_bootstrap":
|
|
agentID := runtimeStringArg(args, "agent_id")
|
|
if agentID == "" {
|
|
return nil, fmt.Errorf("agent_id is required")
|
|
}
|
|
relPath := runtimeStringArg(args, "path")
|
|
if relPath == "" {
|
|
relPath = filepath.ToSlash(filepath.Join("agents", agentID, "AGENT.md"))
|
|
}
|
|
absPath, err := al.resolvePromptFilePath(relPath)
|
|
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
|
|
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":
|
|
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
|
|
case "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.SendTaskMessage(taskID, msg)
|
|
return map[string]interface{}{"ok": ok}, nil
|
|
case "reply":
|
|
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.ReplyToTask(taskID, runtimeStringArg(args, "message_id"), msg)
|
|
return map[string]interface{}{"ok": ok}, nil
|
|
case "ack":
|
|
taskID, err := resolveSubagentTaskIDForRuntime(sm, 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
|
|
case "thread", "trace":
|
|
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)
|
|
}
|
|
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
|
|
case "inbox":
|
|
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
|
|
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 runtimeRawStringArg(args map[string]interface{}, key string) string {
|
|
if args == nil {
|
|
return ""
|
|
}
|
|
v, _ := args[key].(string)
|
|
return 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)
|
|
}
|
|
|
|
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))
|
|
}
|