Created new SubagentTool for synchronous subagent execution: - Implements Tool interface with Name(), Description(), Parameters(), SetContext(), Execute() - Returns ToolResult with ForUser (summary), ForLLM (full details), Silent=false, Async=false - Registered in AgentLoop with context support - Comprehensive test file subagent_tool_test.go with 9 passing tests Acceptance criteria met: - ForUser contains subagent output summary (truncated to 500 chars) - ForLLM contains full execution details with label and result - Typecheck passes (go build ./... succeeds) - go test ./pkg/tools -run TestSubagentTool passes (all 9 tests pass) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
234 lines
5.8 KiB
Go
234 lines
5.8 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
)
|
|
|
|
type SubagentTask struct {
|
|
ID string
|
|
Task string
|
|
Label string
|
|
OriginChannel string
|
|
OriginChatID string
|
|
Status string
|
|
Result string
|
|
Created int64
|
|
}
|
|
|
|
type SubagentManager struct {
|
|
tasks map[string]*SubagentTask
|
|
mu sync.RWMutex
|
|
provider providers.LLMProvider
|
|
bus *bus.MessageBus
|
|
workspace string
|
|
nextID int
|
|
}
|
|
|
|
func NewSubagentManager(provider providers.LLMProvider, workspace string, bus *bus.MessageBus) *SubagentManager {
|
|
return &SubagentManager{
|
|
tasks: make(map[string]*SubagentTask),
|
|
provider: provider,
|
|
bus: bus,
|
|
workspace: workspace,
|
|
nextID: 1,
|
|
}
|
|
}
|
|
|
|
func (sm *SubagentManager) Spawn(ctx context.Context, task, label, originChannel, originChatID string) (string, error) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
|
|
taskID := fmt.Sprintf("subagent-%d", sm.nextID)
|
|
sm.nextID++
|
|
|
|
subagentTask := &SubagentTask{
|
|
ID: taskID,
|
|
Task: task,
|
|
Label: label,
|
|
OriginChannel: originChannel,
|
|
OriginChatID: originChatID,
|
|
Status: "running",
|
|
Created: time.Now().UnixMilli(),
|
|
}
|
|
sm.tasks[taskID] = subagentTask
|
|
|
|
go sm.runTask(ctx, subagentTask)
|
|
|
|
if label != "" {
|
|
return fmt.Sprintf("Spawned subagent '%s' for task: %s", label, task), nil
|
|
}
|
|
return fmt.Sprintf("Spawned subagent for task: %s", task), nil
|
|
}
|
|
|
|
func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) {
|
|
task.Status = "running"
|
|
task.Created = time.Now().UnixMilli()
|
|
|
|
messages := []providers.Message{
|
|
{
|
|
Role: "system",
|
|
Content: "You are a subagent. Complete the given task independently and report the result.",
|
|
},
|
|
{
|
|
Role: "user",
|
|
Content: task.Task,
|
|
},
|
|
}
|
|
|
|
response, err := sm.provider.Chat(ctx, messages, nil, sm.provider.GetDefaultModel(), map[string]interface{}{
|
|
"max_tokens": 4096,
|
|
})
|
|
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
|
|
if err != nil {
|
|
task.Status = "failed"
|
|
task.Result = fmt.Sprintf("Error: %v", err)
|
|
} else {
|
|
task.Status = "completed"
|
|
task.Result = response.Content
|
|
}
|
|
|
|
// Send announce message back to main agent
|
|
if sm.bus != nil {
|
|
announceContent := fmt.Sprintf("Task '%s' completed.\n\nResult:\n%s", task.Label, task.Result)
|
|
sm.bus.PublishInbound(bus.InboundMessage{
|
|
Channel: "system",
|
|
SenderID: fmt.Sprintf("subagent:%s", task.ID),
|
|
// Format: "original_channel:original_chat_id" for routing back
|
|
ChatID: fmt.Sprintf("%s:%s", task.OriginChannel, task.OriginChatID),
|
|
Content: announceContent,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (sm *SubagentManager) GetTask(taskID string) (*SubagentTask, bool) {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
task, ok := sm.tasks[taskID]
|
|
return task, ok
|
|
}
|
|
|
|
func (sm *SubagentManager) ListTasks() []*SubagentTask {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
|
|
tasks := make([]*SubagentTask, 0, len(sm.tasks))
|
|
for _, task := range sm.tasks {
|
|
tasks = append(tasks, task)
|
|
}
|
|
return tasks
|
|
}
|
|
|
|
// SubagentTool executes a subagent task synchronously and returns the result.
|
|
// Unlike SpawnTool which runs tasks asynchronously, SubagentTool waits for completion
|
|
// and returns the result directly in the ToolResult.
|
|
type SubagentTool struct {
|
|
manager *SubagentManager
|
|
originChannel string
|
|
originChatID string
|
|
}
|
|
|
|
func NewSubagentTool(manager *SubagentManager) *SubagentTool {
|
|
return &SubagentTool{
|
|
manager: manager,
|
|
originChannel: "cli",
|
|
originChatID: "direct",
|
|
}
|
|
}
|
|
|
|
func (t *SubagentTool) Name() string {
|
|
return "subagent"
|
|
}
|
|
|
|
func (t *SubagentTool) Description() string {
|
|
return "Execute a subagent task synchronously and return the result. Use this for delegating specific tasks to an independent agent instance. Returns execution summary to user and full details to LLM."
|
|
}
|
|
|
|
func (t *SubagentTool) Parameters() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"task": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "The task for subagent to complete",
|
|
},
|
|
"label": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Optional short label for the task (for display)",
|
|
},
|
|
},
|
|
"required": []string{"task"},
|
|
}
|
|
}
|
|
|
|
func (t *SubagentTool) SetContext(channel, chatID string) {
|
|
t.originChannel = channel
|
|
t.originChatID = chatID
|
|
}
|
|
|
|
func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
|
task, ok := args["task"].(string)
|
|
if !ok {
|
|
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
|
|
}
|
|
|
|
label, _ := args["label"].(string)
|
|
|
|
if t.manager == nil {
|
|
return ErrorResult("Subagent manager not configured").WithError(fmt.Errorf("manager is nil"))
|
|
}
|
|
|
|
// Execute subagent task synchronously via direct provider call
|
|
messages := []providers.Message{
|
|
{
|
|
Role: "system",
|
|
Content: "You are a subagent. Complete the given task independently and provide a clear, concise result.",
|
|
},
|
|
{
|
|
Role: "user",
|
|
Content: task,
|
|
},
|
|
}
|
|
|
|
response, err := t.manager.provider.Chat(ctx, messages, nil, t.manager.provider.GetDefaultModel(), map[string]interface{}{
|
|
"max_tokens": 4096,
|
|
})
|
|
|
|
if err != nil {
|
|
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
|
|
}
|
|
|
|
// ForUser: Brief summary for user (truncated if too long)
|
|
userContent := response.Content
|
|
maxUserLen := 500
|
|
if len(userContent) > maxUserLen {
|
|
userContent = userContent[:maxUserLen] + "..."
|
|
}
|
|
|
|
// ForLLM: Full execution details
|
|
llmContent := fmt.Sprintf("Subagent task completed:\nLabel: %s\nResult: %s",
|
|
func() string {
|
|
if label != "" {
|
|
return label
|
|
}
|
|
return "(unnamed)"
|
|
}(),
|
|
response.Content)
|
|
|
|
return &ToolResult{
|
|
ForLLM: llmContent,
|
|
ForUser: userContent,
|
|
Silent: false,
|
|
IsError: false,
|
|
Async: false,
|
|
}
|
|
}
|