mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(agent): wire SubTurn into AgentLoop and Spawn Tool
- Add subTurnResults sync.Map to AgentLoop for per-session channel tracking - Add register/unregister/dequeue methods in steering.go - Poll SubTurn results in runLLMIteration at loop start and after each tool, injecting results as [SubTurn Result] messages into parent conversation - Initialize root turnState in runAgentLoop, propagate via context (withTurnState/turnStateFromContext), call rootTS.Finish() on completion - Wire Spawn Tool to spawnSubTurn via SetSpawner in registerSharedTools, recovering parentTS from context for proper turn hierarchy - Refactor subagent.go to use SetSpawner pattern - Add TestSubTurnResultChannelRegistration and TestDequeuePendingSubTurnResults
This commit is contained in:
+111
-64
@@ -21,6 +21,15 @@ type SubagentTask struct {
|
||||
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
|
||||
@@ -34,6 +43,7 @@ type SubagentManager struct {
|
||||
hasMaxTokens bool
|
||||
hasTemperature bool
|
||||
nextID int
|
||||
spawner SpawnSubTurnFunc
|
||||
}
|
||||
|
||||
func NewSubagentManager(
|
||||
@@ -51,6 +61,12 @@ func NewSubagentManager(
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -112,22 +128,6 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask, call
|
||||
task.Status = "running"
|
||||
task.Created = time.Now().UnixMilli()
|
||||
|
||||
// Build system prompt for subagent
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
// Check if context is already canceled before starting
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -139,8 +139,8 @@ After completing the task, provide a clear summary of what was done.`
|
||||
default:
|
||||
}
|
||||
|
||||
// Run tool loop with access to tools
|
||||
sm.mu.RLock()
|
||||
spawner := sm.spawner
|
||||
tools := sm.tools
|
||||
maxIter := sm.maxIterations
|
||||
maxTokens := sm.maxTokens
|
||||
@@ -149,27 +149,59 @@ After completing the task, provide a clear summary of what was done.`
|
||||
hasTemperature := sm.hasTemperature
|
||||
sm.mu.RUnlock()
|
||||
|
||||
var llmOptions map[string]any
|
||||
if hasMaxTokens || hasTemperature {
|
||||
llmOptions = map[string]any{}
|
||||
if hasMaxTokens {
|
||||
llmOptions["max_tokens"] = maxTokens
|
||||
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},
|
||||
}
|
||||
if hasTemperature {
|
||||
llmOptions["temperature"] = temperature
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loopResult, err := RunToolLoop(ctx, ToolLoopConfig{
|
||||
Provider: sm.provider,
|
||||
Model: sm.defaultModel,
|
||||
Tools: tools,
|
||||
MaxIterations: maxIter,
|
||||
LLMOptions: llmOptions,
|
||||
}, messages, task.OriginChannel, task.OriginChatID)
|
||||
|
||||
sm.mu.Lock()
|
||||
var result *ToolResult
|
||||
defer func() {
|
||||
sm.mu.Unlock()
|
||||
// Call callback if provided and result is set
|
||||
@@ -196,19 +228,7 @@ After completing the task, provide a clear summary of what was done.`
|
||||
}
|
||||
} else {
|
||||
task.Status = "completed"
|
||||
task.Result = loopResult.Content
|
||||
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,
|
||||
}
|
||||
task.Result = result.ForLLM
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,8 +251,6 @@ func (sm *SubagentManager) ListTasks() []*SubagentTask {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -280,7 +298,51 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||
return ErrorResult("Subagent manager not configured").WithError(fmt.Errorf("manager is nil"))
|
||||
}
|
||||
|
||||
// Build messages for subagent
|
||||
sm := t.manager
|
||||
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()
|
||||
|
||||
if spawner != nil {
|
||||
// Use spawner
|
||||
res, err := spawner(ctx, task, label, "", tools, maxTokens, temperature, hasMaxTokens, hasTemperature)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
|
||||
}
|
||||
|
||||
// Ensure synchronous ForUser display truncates
|
||||
userContent := res.ForLLM
|
||||
if res.ForUser != "" {
|
||||
userContent = res.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, res.ForLLM)
|
||||
|
||||
return &ToolResult{
|
||||
ForLLM: llmContent,
|
||||
ForUser: userContent,
|
||||
Silent: false,
|
||||
IsError: res.IsError,
|
||||
Async: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Build messages for subagent fallback
|
||||
messages := []providers.Message{
|
||||
{
|
||||
Role: "system",
|
||||
@@ -292,17 +354,6 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||
},
|
||||
}
|
||||
|
||||
// Use RunToolLoop to execute with tools (same as async SpawnTool)
|
||||
sm := t.manager
|
||||
sm.mu.RLock()
|
||||
tools := sm.tools
|
||||
maxIter := sm.maxIterations
|
||||
maxTokens := sm.maxTokens
|
||||
temperature := sm.temperature
|
||||
hasMaxTokens := sm.hasMaxTokens
|
||||
hasTemperature := sm.hasTemperature
|
||||
sm.mu.RUnlock()
|
||||
|
||||
var llmOptions map[string]any
|
||||
if hasMaxTokens || hasTemperature {
|
||||
llmOptions = map[string]any{}
|
||||
@@ -314,8 +365,6 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to "cli"/"direct" for non-conversation callers (e.g., CLI, tests)
|
||||
// to preserve the same defaults as the original NewSubagentTool constructor.
|
||||
channel := ToolChannel(ctx)
|
||||
if channel == "" {
|
||||
channel = "cli"
|
||||
@@ -336,14 +385,12 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
|
||||
}
|
||||
|
||||
// ForUser: Brief summary for user (truncated if too long)
|
||||
userContent := loopResult.Content
|
||||
maxUserLen := 500
|
||||
if len(userContent) > maxUserLen {
|
||||
userContent = userContent[:maxUserLen] + "..."
|
||||
}
|
||||
|
||||
// ForLLM: Full execution details
|
||||
labelStr := label
|
||||
if labelStr == "" {
|
||||
labelStr = "(unnamed)"
|
||||
|
||||
Reference in New Issue
Block a user