All checks were successful
Stable Release / stable (push) Successful in 1m34s
Major additions: - RAG pipeline (indexing, chunking, search) with sidebar upload button - Memory system with CRUD API - Plugins and lessons modules - MCP discovery and MCP server - Advanced skills (auto-create, conditional, improver) - Agent browser/image support, delegate, sessions - File editor with CodeMirror in split panes - Markdown rendering via react-markdown + KaTeX + highlight.js - Raw markdown toggle - PWA manifest + service worker - Extension UI redesign with new design tokens and studio-style chat - Pipeline API for chat streaming - Mobile responsive layout 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
204 lines
5.4 KiB
Go
204 lines
5.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type DelegateTaskParams struct {
|
|
Task string `json:"task" description:"Description of the sub-task to delegate"`
|
|
Context string `json:"context,omitempty" description:"Additional context for the sub-task"`
|
|
Timeout int `json:"timeout,omitempty" description:"Timeout per sub-task in seconds (default 120, max 300)"`
|
|
MaxParallel int `json:"max_parallel,omitempty" description:"Maximum parallel sub-tasks (default 3, max 5)"`
|
|
}
|
|
|
|
type DelegateMultiParams struct {
|
|
Tasks []DelegateTaskParams `json:"tasks" description:"List of sub-tasks to execute in parallel"`
|
|
MaxParallel int `json:"max_parallel,omitempty" description:"Maximum parallel sub-tasks (default 3, max 5)"`
|
|
}
|
|
|
|
type SubTaskResult struct {
|
|
Task string `json:"task"`
|
|
Success bool `json:"success"`
|
|
Result string `json:"result"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type DelegateResponse struct {
|
|
TotalTasks int `json:"total_tasks"`
|
|
Successful int `json:"successful"`
|
|
Failed int `json:"failed"`
|
|
Results []SubTaskResult `json:"results"`
|
|
Duration string `json:"duration"`
|
|
}
|
|
|
|
func NewDelegateTool(registry *Registry) (*ToolDefinition, error) {
|
|
return NewTool("delegate_task",
|
|
"Delegate one or more tasks for parallel execution. Each sub-task runs in isolation with its own context. Returns aggregated results from all sub-tasks. Use for independent tasks that can run concurrently.",
|
|
func(ctx context.Context, p DelegateTaskParams) (ToolResponse, error) {
|
|
if p.Task == "" {
|
|
return TextErrorResponse("task is required"), nil
|
|
}
|
|
|
|
timeout := time.Duration(p.Timeout) * time.Second
|
|
if timeout == 0 {
|
|
timeout = 120 * time.Second
|
|
}
|
|
if timeout > 300*time.Second {
|
|
timeout = 300 * time.Second
|
|
}
|
|
|
|
result := executeSubTask(ctx, p.Task, p.Context, timeout, registry)
|
|
resp := DelegateResponse{
|
|
TotalTasks: 1,
|
|
Successful: 0,
|
|
Results: []SubTaskResult{result},
|
|
Duration: "N/A",
|
|
}
|
|
if result.Success {
|
|
resp.Successful = 1
|
|
} else {
|
|
resp.Failed = 1
|
|
}
|
|
|
|
data, _ := json.MarshalIndent(resp, "", " ")
|
|
return TextResponse(string(data)), nil
|
|
})
|
|
}
|
|
|
|
func NewDelegateMultiTool(registry *Registry) (*ToolDefinition, error) {
|
|
return NewTool("delegate_multi",
|
|
"Execute multiple independent tasks in parallel using goroutines. Each task runs in its own isolated context. Returns aggregated results. Use for batch operations, parallel analysis, or concurrent file processing.",
|
|
func(ctx context.Context, p DelegateMultiParams) (ToolResponse, error) {
|
|
if len(p.Tasks) == 0 {
|
|
return TextErrorResponse("tasks list is required"), nil
|
|
}
|
|
|
|
maxParallel := p.MaxParallel
|
|
if maxParallel <= 0 {
|
|
maxParallel = 3
|
|
}
|
|
if maxParallel > 5 {
|
|
maxParallel = 5
|
|
}
|
|
|
|
if len(p.Tasks) > 10 {
|
|
return TextErrorResponse("maximum 10 tasks per delegation"), nil
|
|
}
|
|
|
|
start := time.Now()
|
|
results := executeParallelTasks(ctx, p.Tasks, maxParallel, registry)
|
|
duration := time.Since(start)
|
|
|
|
resp := DelegateResponse{
|
|
TotalTasks: len(results),
|
|
Results: results,
|
|
Duration: duration.Round(time.Millisecond).String(),
|
|
}
|
|
|
|
for _, r := range results {
|
|
if r.Success {
|
|
resp.Successful++
|
|
} else {
|
|
resp.Failed++
|
|
}
|
|
}
|
|
|
|
data, _ := json.MarshalIndent(resp, "", " ")
|
|
return TextResponse(string(data)), nil
|
|
})
|
|
}
|
|
|
|
func executeSubTask(ctx context.Context, task, contextInfo string, timeout time.Duration, registry *Registry) SubTaskResult {
|
|
taskCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
result := SubTaskResult{
|
|
Task: truncateString(task, 100),
|
|
}
|
|
|
|
if contextInfo != "" {
|
|
result.Task = fmt.Sprintf("%s (context: %s)", result.Task, truncateString(contextInfo, 50))
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
|
|
terminalTool, ok := registry.Get("terminal")
|
|
if !ok {
|
|
result.Error = "terminal tool not available"
|
|
return
|
|
}
|
|
|
|
args, _ := json.Marshal(TerminalParams{
|
|
Command: task,
|
|
Timeout: int(timeout.Seconds()),
|
|
})
|
|
|
|
resp, err := terminalTool.Execute(taskCtx, ToolCall{
|
|
ID: fmt.Sprintf("delegate_%d", time.Now().UnixNano()),
|
|
Name: "terminal",
|
|
Arguments: args,
|
|
})
|
|
if err != nil {
|
|
result.Error = err.Error()
|
|
return
|
|
}
|
|
|
|
result.Result = resp.Content
|
|
result.Success = !resp.IsError
|
|
if resp.IsError {
|
|
result.Error = resp.Content
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
return result
|
|
case <-taskCtx.Done():
|
|
result.Error = fmt.Sprintf("sub-task timed out after %v", timeout)
|
|
return result
|
|
}
|
|
}
|
|
|
|
func executeParallelTasks(ctx context.Context, tasks []DelegateTaskParams, maxParallel int, registry *Registry) []SubTaskResult {
|
|
results := make([]SubTaskResult, len(tasks))
|
|
|
|
sem := make(chan struct{}, maxParallel)
|
|
var wg sync.WaitGroup
|
|
|
|
for i, task := range tasks {
|
|
wg.Add(1)
|
|
go func(idx int, t DelegateTaskParams) {
|
|
defer wg.Done()
|
|
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
|
|
timeout := time.Duration(t.Timeout) * time.Second
|
|
if timeout == 0 {
|
|
timeout = 120 * time.Second
|
|
}
|
|
if timeout > 300*time.Second {
|
|
timeout = 300 * time.Second
|
|
}
|
|
|
|
results[idx] = executeSubTask(ctx, t.Task, t.Context, timeout, registry)
|
|
}(i, task)
|
|
}
|
|
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
func truncateString(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen] + "..."
|
|
}
|