mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 16:15:24 +00:00
* feat(collaboration): isolate delegated artifacts and child runs Isolate delegated inputs and outputs behind secure artifact exchange lifecycles. Scope Agent Link tasks by tenant and root agent, and enforce delegation spawn-tree boundaries. Add process-wide child-run admission and preserve logical media paths across native, MCP, and sandbox execution. * fix(collaboration): harden delegated task isolation Enforce tenant and root-agent task scope across migrations and stores. Add exactly-once async completion delivery, delegated sandbox boundaries, and confined artifact and media recovery across runtime surfaces. * fix(collaboration): recover interrupted async tasks * fix(collaboration): normalize persisted child-run status --------- Co-authored-by: ntduc <ntduc@cpp.ai.vn>
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// executeList shows running and retained subagent tasks.
|
|
func (t *SpawnTool) executeList(ctx context.Context) *Result {
|
|
scope := t.scopeFromContext(ctx)
|
|
tasks := t.subagentMgr.ListTasks(scope, "")
|
|
if len(tasks) == 0 {
|
|
return &Result{ForLLM: "No subagent tasks found."}
|
|
}
|
|
|
|
var lines []string
|
|
queued, running, completed, failed, cancelled := 0, 0, 0, 0, 0
|
|
for _, task := range tasks {
|
|
switch task.Status {
|
|
case TaskStatusQueued:
|
|
queued++
|
|
case TaskStatusRunning:
|
|
running++
|
|
case TaskStatusWaiting:
|
|
running++
|
|
case TaskStatusCompleted:
|
|
completed++
|
|
case TaskStatusFailed:
|
|
failed++
|
|
case TaskStatusCancelled:
|
|
cancelled++
|
|
}
|
|
line := fmt.Sprintf("- [%s] %s (id=%s, status=%s)", task.Label, truncate(task.Task, 60), task.ID, task.Status)
|
|
if task.CompletedAt > 0 {
|
|
dur := time.Duration(task.CompletedAt-task.CreatedAt) * time.Millisecond
|
|
line += fmt.Sprintf(", took %s", dur.Round(time.Millisecond))
|
|
}
|
|
lines = append(lines, line)
|
|
}
|
|
|
|
return &Result{ForLLM: fmt.Sprintf(
|
|
"Subagent tasks: %d queued, %d active, %d completed, %d failed, %d cancelled (%d retained total)\n%s",
|
|
queued, running, completed, failed, cancelled, len(tasks), strings.Join(lines, "\n"),
|
|
)}
|
|
}
|
|
|
|
// executeCancel cancels a subagent task by ID.
|
|
func (t *SpawnTool) executeCancel(ctx context.Context, args map[string]any) *Result {
|
|
id, _ := args["id"].(string)
|
|
if id == "" {
|
|
return ErrorResult("id is required for action=cancel")
|
|
}
|
|
|
|
if t.subagentMgr.CancelTask(t.scopeFromContext(ctx), id) {
|
|
return &Result{ForLLM: fmt.Sprintf("Task '%s' cancelled.", id)}
|
|
}
|
|
|
|
return ErrorResult(fmt.Sprintf("Task '%s' not found or not running.", id))
|
|
}
|
|
|
|
// executeSteer redirects a running subagent with new instructions.
|
|
func (t *SpawnTool) executeSteer(ctx context.Context, args map[string]any) *Result {
|
|
id, _ := args["id"].(string)
|
|
if id == "" {
|
|
return ErrorResult("id is required for action=steer")
|
|
}
|
|
message, _ := args["message"].(string)
|
|
if message == "" {
|
|
return ErrorResult("message is required for action=steer")
|
|
}
|
|
|
|
msg, err := t.subagentMgr.Steer(ctx, t.scopeFromContext(ctx), id, message, nil)
|
|
if err != nil {
|
|
return ErrorResult(err.Error())
|
|
}
|
|
return &Result{ForLLM: msg}
|
|
}
|
|
|
|
// executeWait blocks until all children of the calling agent complete or timeout.
|
|
func (t *SpawnTool) executeWait(ctx context.Context, args map[string]any) *Result {
|
|
timeout := 300
|
|
if v, ok := args["timeout"].(float64); ok && v > 0 {
|
|
timeout = int(v)
|
|
}
|
|
|
|
tasks, err := t.subagentMgr.WaitForChildren(ctx, t.scopeFromContext(ctx), "", timeout)
|
|
return t.formatWaitResult(tasks, err)
|
|
}
|
|
|
|
func (t *SpawnTool) formatWaitResult(tasks []*SubagentTask, waitErr error) *Result {
|
|
if len(tasks) == 0 {
|
|
msg := "No subagent tasks found."
|
|
if waitErr != nil {
|
|
msg += " Error: " + waitErr.Error()
|
|
}
|
|
return &Result{ForLLM: msg}
|
|
}
|
|
|
|
var sb strings.Builder
|
|
completed, failed := 0, 0
|
|
var totalIn, totalOut int64
|
|
for _, task := range tasks {
|
|
switch task.Status {
|
|
case TaskStatusCompleted:
|
|
completed++
|
|
case TaskStatusFailed:
|
|
failed++
|
|
}
|
|
totalIn += task.TotalInputTokens
|
|
totalOut += task.TotalOutputTokens
|
|
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", task.Status, task.Label, truncate(task.Result, 200)))
|
|
}
|
|
|
|
header := fmt.Sprintf("All %d subagent tasks finished (%d completed, %d failed). Total tokens: %d in / %d out\n\n",
|
|
len(tasks), completed, failed, totalIn, totalOut)
|
|
if waitErr != nil {
|
|
header = fmt.Sprintf("Wait ended: %s. %d tasks total.\n\n", waitErr.Error(), len(tasks))
|
|
}
|
|
|
|
return &Result{ForLLM: header + sb.String()}
|
|
}
|
|
|
|
// FilterDenyList returns tool names from the registry excluding denied tools.
|
|
func FilterDenyList(reg *Registry, denyList []string) []string {
|
|
deny := make(map[string]bool, len(denyList))
|
|
for _, n := range denyList {
|
|
deny[n] = true
|
|
}
|
|
|
|
var allowed []string
|
|
for _, name := range reg.List() {
|
|
if !deny[name] {
|
|
allowed = append(allowed, name)
|
|
}
|
|
}
|
|
return allowed
|
|
}
|
|
|
|
// IsSubagentDenied checks if a tool name is in the subagent deny list.
|
|
func IsSubagentDenied(toolName string, depth, maxDepth int) bool {
|
|
for _, d := range SubagentDenyAlways {
|
|
if strings.EqualFold(toolName, d) {
|
|
return true
|
|
}
|
|
}
|
|
if depth >= maxDepth {
|
|
for _, d := range SubagentDenyLeaf {
|
|
if strings.EqualFold(toolName, d) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|