mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
411 lines
11 KiB
Go
411 lines
11 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
)
|
|
|
|
// SubTurnSpawner is an interface for spawning sub-turns.
|
|
// This avoids circular dependency between tools and agent packages.
|
|
type SubTurnSpawner interface {
|
|
SpawnSubTurn(ctx context.Context, cfg SubTurnConfig) (*ToolResult, error)
|
|
}
|
|
|
|
// SubTurnConfig holds configuration for spawning a sub-turn.
|
|
type SubTurnConfig struct {
|
|
Model string
|
|
Tools []Tool
|
|
SystemPrompt string
|
|
MaxTokens int
|
|
Temperature float64
|
|
Async bool // true for async (spawn), false for sync (subagent)
|
|
Critical bool // continue running after parent finishes gracefully
|
|
Timeout time.Duration // 0 = use default (5 minutes)
|
|
MaxContextRunes int // 0 = auto, -1 = no limit, >0 = explicit limit
|
|
ActualSystemPrompt string
|
|
InitialMessages []providers.Message
|
|
InitialTokenBudget *atomic.Int64 // Shared token budget for team members; nil if no budget
|
|
}
|
|
|
|
type SubagentTask struct {
|
|
ID string
|
|
Task string
|
|
Label string
|
|
AgentID string
|
|
OriginChannel string
|
|
OriginChatID string
|
|
Status string
|
|
Result string
|
|
Created int64
|
|
}
|
|
|
|
type SpawnSubTurnFunc func(
|
|
ctx context.Context,
|
|
task, label, agentID string,
|
|
tools *ToolRegistry,
|
|
maxTokens int,
|
|
temperature float64,
|
|
hasMaxTokens, hasTemperature bool,
|
|
) (*ToolResult, error)
|
|
|
|
type SubagentManager struct {
|
|
tasks map[string]*SubagentTask
|
|
mu sync.RWMutex
|
|
provider providers.LLMProvider
|
|
defaultModel string
|
|
workspace string
|
|
tools *ToolRegistry
|
|
maxIterations int
|
|
maxTokens int
|
|
temperature float64
|
|
hasMaxTokens bool
|
|
hasTemperature bool
|
|
nextID int
|
|
spawner SpawnSubTurnFunc
|
|
}
|
|
|
|
func NewSubagentManager(
|
|
provider providers.LLMProvider,
|
|
defaultModel, workspace string,
|
|
) *SubagentManager {
|
|
return &SubagentManager{
|
|
tasks: make(map[string]*SubagentTask),
|
|
provider: provider,
|
|
defaultModel: defaultModel,
|
|
workspace: workspace,
|
|
tools: NewToolRegistry(),
|
|
maxIterations: 10,
|
|
nextID: 1,
|
|
}
|
|
}
|
|
|
|
func (sm *SubagentManager) SetSpawner(spawner SpawnSubTurnFunc) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
sm.spawner = spawner
|
|
}
|
|
|
|
// SetLLMOptions sets max tokens and temperature for subagent LLM calls.
|
|
func (sm *SubagentManager) SetLLMOptions(maxTokens int, temperature float64) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
sm.maxTokens = maxTokens
|
|
sm.hasMaxTokens = true
|
|
sm.temperature = temperature
|
|
sm.hasTemperature = true
|
|
}
|
|
|
|
// SetTools sets the tool registry for subagent execution.
|
|
// If not set, subagent will have access to the provided tools.
|
|
func (sm *SubagentManager) SetTools(tools *ToolRegistry) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
sm.tools = tools
|
|
}
|
|
|
|
// RegisterTool registers a tool for subagent execution.
|
|
func (sm *SubagentManager) RegisterTool(tool Tool) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
sm.tools.Register(tool)
|
|
}
|
|
|
|
func (sm *SubagentManager) Spawn(
|
|
ctx context.Context,
|
|
task, label, agentID, originChannel, originChatID string,
|
|
callback AsyncCallback,
|
|
) (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,
|
|
AgentID: agentID,
|
|
OriginChannel: originChannel,
|
|
OriginChatID: originChatID,
|
|
Status: "running",
|
|
Created: time.Now().UnixMilli(),
|
|
}
|
|
sm.tasks[taskID] = subagentTask
|
|
|
|
// Start task in background with context cancellation support
|
|
go sm.runTask(ctx, subagentTask, callback)
|
|
|
|
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, callback AsyncCallback) {
|
|
task.Status = "running"
|
|
task.Created = time.Now().UnixMilli()
|
|
|
|
// Check if context is already canceled before starting
|
|
select {
|
|
case <-ctx.Done():
|
|
sm.mu.Lock()
|
|
task.Status = "canceled"
|
|
task.Result = "Task canceled before execution"
|
|
sm.mu.Unlock()
|
|
return
|
|
default:
|
|
}
|
|
|
|
sm.mu.RLock()
|
|
spawner := sm.spawner
|
|
tools := sm.tools
|
|
maxIter := sm.maxIterations
|
|
maxTokens := sm.maxTokens
|
|
temperature := sm.temperature
|
|
hasMaxTokens := sm.hasMaxTokens
|
|
hasTemperature := sm.hasTemperature
|
|
sm.mu.RUnlock()
|
|
|
|
var result *ToolResult
|
|
var err error
|
|
|
|
if spawner != nil {
|
|
result, err = spawner(ctx, task.Task, task.Label, task.AgentID, tools, maxTokens, temperature, hasMaxTokens, hasTemperature)
|
|
} else {
|
|
// Fallback to legacy RunToolLoop
|
|
systemPrompt := `You are a subagent. Complete the given task independently and report the result.
|
|
You have access to tools - use them as needed to complete your task.
|
|
After completing the task, provide a clear summary of what was done.`
|
|
|
|
messages := []providers.Message{
|
|
{Role: "system", Content: systemPrompt},
|
|
{Role: "user", Content: task.Task},
|
|
}
|
|
|
|
var llmOptions map[string]any
|
|
if hasMaxTokens || hasTemperature {
|
|
llmOptions = map[string]any{}
|
|
if hasMaxTokens {
|
|
llmOptions["max_tokens"] = maxTokens
|
|
}
|
|
if hasTemperature {
|
|
llmOptions["temperature"] = temperature
|
|
}
|
|
}
|
|
|
|
var loopResult *ToolLoopResult
|
|
loopResult, err = RunToolLoop(ctx, ToolLoopConfig{
|
|
Provider: sm.provider,
|
|
Model: sm.defaultModel,
|
|
Tools: tools,
|
|
MaxIterations: maxIter,
|
|
LLMOptions: llmOptions,
|
|
}, messages, task.OriginChannel, task.OriginChatID)
|
|
|
|
if err == nil {
|
|
result = &ToolResult{
|
|
ForLLM: fmt.Sprintf(
|
|
"Subagent '%s' completed (iterations: %d): %s",
|
|
task.Label,
|
|
loopResult.Iterations,
|
|
loopResult.Content,
|
|
),
|
|
ForUser: loopResult.Content,
|
|
Silent: false,
|
|
IsError: false,
|
|
Async: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
sm.mu.Lock()
|
|
defer func() {
|
|
sm.mu.Unlock()
|
|
// Call callback if provided and result is set
|
|
if callback != nil && result != nil {
|
|
callback(ctx, result)
|
|
}
|
|
}()
|
|
|
|
if err != nil {
|
|
task.Status = "failed"
|
|
task.Result = fmt.Sprintf("Error: %v", err)
|
|
// Check if it was canceled
|
|
if ctx.Err() != nil {
|
|
task.Status = "canceled"
|
|
task.Result = "Task canceled during execution"
|
|
}
|
|
result = &ToolResult{
|
|
ForLLM: task.Result,
|
|
ForUser: "",
|
|
Silent: false,
|
|
IsError: true,
|
|
Async: false,
|
|
Err: err,
|
|
}
|
|
} else {
|
|
task.Status = "completed"
|
|
task.Result = result.ForLLM
|
|
}
|
|
}
|
|
|
|
func (sm *SubagentManager) GetTask(taskID string) (*SubagentTask, bool) {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
task, ok := sm.tasks[taskID]
|
|
return task, ok
|
|
}
|
|
|
|
// GetTaskCopy returns a copy of the task with the given ID, taken under the
|
|
// read lock, so the caller receives a consistent snapshot with no data race.
|
|
func (sm *SubagentManager) GetTaskCopy(taskID string) (SubagentTask, bool) {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
task, ok := sm.tasks[taskID]
|
|
if !ok {
|
|
return SubagentTask{}, false
|
|
}
|
|
return *task, true
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// ListTaskCopies returns value copies of all tasks, taken under the read lock,
|
|
// so callers receive consistent snapshots with no data race.
|
|
func (sm *SubagentManager) ListTaskCopies() []SubagentTask {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
|
|
copies := make([]SubagentTask, 0, len(sm.tasks))
|
|
for _, task := range sm.tasks {
|
|
copies = append(copies, *task)
|
|
}
|
|
return copies
|
|
}
|
|
|
|
// SubagentTool executes a subagent task synchronously and returns the result.
|
|
// It directly calls SubTurnSpawner with Async=false for synchronous execution.
|
|
type SubagentTool struct {
|
|
spawner SubTurnSpawner
|
|
defaultModel string
|
|
maxTokens int
|
|
temperature float64
|
|
}
|
|
|
|
func NewSubagentTool(manager *SubagentManager) *SubagentTool {
|
|
return &SubagentTool{
|
|
defaultModel: manager.defaultModel,
|
|
maxTokens: manager.maxTokens,
|
|
temperature: manager.temperature,
|
|
}
|
|
}
|
|
|
|
// SetSpawner sets the SubTurnSpawner for direct sub-turn execution.
|
|
func (t *SubagentTool) SetSpawner(spawner SubTurnSpawner) {
|
|
t.spawner = spawner
|
|
}
|
|
|
|
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]any {
|
|
return map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"task": map[string]any{
|
|
"type": "string",
|
|
"description": "The task for subagent to complete",
|
|
},
|
|
"label": map[string]any{
|
|
"type": "string",
|
|
"description": "Optional short label for the task (for display)",
|
|
},
|
|
},
|
|
"required": []string{"task"},
|
|
}
|
|
}
|
|
|
|
func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
|
task, ok := args["task"].(string)
|
|
if !ok {
|
|
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
|
|
}
|
|
|
|
label, _ := args["label"].(string)
|
|
|
|
// Build system prompt for subagent
|
|
systemPrompt := fmt.Sprintf(`You are a subagent. Complete the given task independently and provide a clear, concise result.
|
|
|
|
Task: %s`, task)
|
|
|
|
if label != "" {
|
|
systemPrompt = fmt.Sprintf(`You are a subagent labeled "%s". Complete the given task independently and provide a clear, concise result.
|
|
|
|
Task: %s`, label, task)
|
|
}
|
|
|
|
// Use spawner if available (direct SpawnSubTurn call)
|
|
if t.spawner != nil {
|
|
result, err := t.spawner.SpawnSubTurn(ctx, SubTurnConfig{
|
|
Model: t.defaultModel,
|
|
Tools: nil, // Will inherit from parent via context
|
|
SystemPrompt: systemPrompt,
|
|
MaxTokens: t.maxTokens,
|
|
Temperature: t.temperature,
|
|
Async: false, // Synchronous execution
|
|
})
|
|
|
|
if err != nil {
|
|
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
|
|
}
|
|
|
|
// Format result for display
|
|
userContent := result.ForLLM
|
|
if result.ForUser != "" {
|
|
userContent = result.ForUser
|
|
}
|
|
maxUserLen := 500
|
|
if len(userContent) > maxUserLen {
|
|
userContent = userContent[:maxUserLen] + "..."
|
|
}
|
|
|
|
labelStr := label
|
|
if labelStr == "" {
|
|
labelStr = "(unnamed)"
|
|
}
|
|
llmContent := fmt.Sprintf("Subagent task completed:\nLabel: %s\nResult: %s",
|
|
labelStr, result.ForLLM)
|
|
|
|
return &ToolResult{
|
|
ForLLM: llmContent,
|
|
ForUser: userContent,
|
|
Silent: false,
|
|
IsError: result.IsError,
|
|
Async: false,
|
|
}
|
|
}
|
|
|
|
// Fallback: spawner not configured
|
|
return ErrorResult("SubagentTool: spawner not configured - call SetSpawner() during initialization").WithError(fmt.Errorf("spawner not set"))
|
|
}
|