feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
All checks were successful
Stable Release / stable (push) Successful in 1m34s
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>
This commit is contained in:
203
internal/agent/delegate.go
Normal file
203
internal/agent/delegate.go
Normal file
@@ -0,0 +1,203 @@
|
||||
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] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user