mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(subturn): support stateful iteration for evaluator-optimizer pattern
Add ActualSystemPrompt and InitialMessages fields to SubTurnConfig to enable stateful worker context passing across multiple evaluation iterations. Changes: - Add ActualSystemPrompt field to separate system role from user task description - Add InitialMessages field to preload ephemeral session history before agent loop starts - Add Messages field to ToolResult for carrying session history (internal use, not serialized) - Update runTurn to inject system prompt and preload history from InitialMessages - Update AgentLoopSpawner to map new fields from tools.SubTurnConfig to agent.SubTurnConfig This enables the evaluator-optimizer execution strategy in team tool to maintain worker context across iterations while keeping SubTurn isolation intact.
This commit is contained in:
@@ -64,6 +64,7 @@ type processOptions struct {
|
||||
SenderID string // Current sender ID for dynamic context
|
||||
SenderDisplayName string // Current sender display name for dynamic context
|
||||
UserMessage string // User message content (may include prefix)
|
||||
SystemPromptOverride string // Override the default system prompt (Used by SubTurns)
|
||||
Media []string // media:// refs from inbound message
|
||||
DefaultResponse string // Response when LLM returns empty
|
||||
EnableSummary bool // Whether to trigger summarization
|
||||
@@ -1069,6 +1070,17 @@ func (al *AgentLoop) runAgentLoop(
|
||||
maxMediaSize := cfg.Agents.Defaults.GetMaxMediaSize()
|
||||
messages = resolveMediaRefs(messages, al.mediaStore, maxMediaSize)
|
||||
|
||||
// 1.5 Override the System prompt (e.g., for Evaluator/Optimizer specific personas)
|
||||
if opts.SystemPromptOverride != "" {
|
||||
for i, msg := range messages {
|
||||
if msg.Role == "system" {
|
||||
messages[i].Content = opts.SystemPromptOverride
|
||||
messages[i].SystemParts = []providers.ContentBlock{{Type: "text", Text: opts.SystemPromptOverride}}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Save user message to session
|
||||
if !opts.SkipAddUserMessage && opts.UserMessage != "" {
|
||||
agent.Sessions.AddMessage(opts.SessionKey, "user", opts.UserMessage)
|
||||
|
||||
+43
-17
@@ -119,6 +119,14 @@ type SubTurnConfig struct {
|
||||
// truncated while preserving system messages and recent context.
|
||||
MaxContextRunes int
|
||||
|
||||
// ActualSystemPrompt is injected as the true 'system' role message for the childAgent.
|
||||
// The legacy SystemPrompt field is actually used as the first 'user' message (task description).
|
||||
ActualSystemPrompt string
|
||||
|
||||
// InitialMessages preloads the ephemeral session history before the agent loop starts.
|
||||
// Used by evaluator-optimizer patterns to pass the full worker context across multiple iterations.
|
||||
InitialMessages []providers.Message
|
||||
|
||||
// Can be extended with temperature, topP, etc.
|
||||
}
|
||||
|
||||
@@ -186,14 +194,16 @@ func (s *AgentLoopSpawner) SpawnSubTurn(ctx context.Context, cfg tools.SubTurnCo
|
||||
|
||||
// Convert tools.SubTurnConfig to agent.SubTurnConfig
|
||||
agentCfg := SubTurnConfig{
|
||||
Model: cfg.Model,
|
||||
Tools: cfg.Tools,
|
||||
SystemPrompt: cfg.SystemPrompt,
|
||||
MaxTokens: cfg.MaxTokens,
|
||||
Async: cfg.Async,
|
||||
Critical: cfg.Critical,
|
||||
Timeout: cfg.Timeout,
|
||||
MaxContextRunes: cfg.MaxContextRunes,
|
||||
Model: cfg.Model,
|
||||
Tools: cfg.Tools,
|
||||
SystemPrompt: cfg.SystemPrompt,
|
||||
ActualSystemPrompt: cfg.ActualSystemPrompt,
|
||||
InitialMessages: cfg.InitialMessages,
|
||||
MaxTokens: cfg.MaxTokens,
|
||||
Async: cfg.Async,
|
||||
Critical: cfg.Critical,
|
||||
Timeout: cfg.Timeout,
|
||||
MaxContextRunes: cfg.MaxContextRunes,
|
||||
}
|
||||
|
||||
return spawnSubTurn(ctx, s.al, parentTS, agentCfg)
|
||||
@@ -481,6 +491,19 @@ func runTurn(ctx context.Context, al *AgentLoop, ts *turnState, cfg SubTurnConfi
|
||||
childAgent.MaxTokens = parentAgent.MaxTokens
|
||||
}
|
||||
|
||||
if cfg.ActualSystemPrompt != "" {
|
||||
childAgent.Sessions.AddMessage(ts.turnID, "system", cfg.ActualSystemPrompt)
|
||||
}
|
||||
|
||||
promptAlreadyAdded := false
|
||||
|
||||
// Preload ephemeral session history
|
||||
if len(cfg.InitialMessages) > 0 {
|
||||
existing := childAgent.Sessions.GetHistory(ts.turnID)
|
||||
childAgent.Sessions.SetHistory(ts.turnID, append(existing, cfg.InitialMessages...))
|
||||
promptAlreadyAdded = true // InitialMessages 中已含 user 消息,跳过再次添加
|
||||
}
|
||||
|
||||
// Resolve MaxContextRunes configuration
|
||||
maxContextRunes := utils.ResolveMaxContextRunes(cfg.MaxContextRunes, childAgent.ContextWindow)
|
||||
|
||||
@@ -501,7 +524,6 @@ func runTurn(ctx context.Context, al *AgentLoop, ts *turnState, cfg SubTurnConfi
|
||||
truncationRetryCount := 0
|
||||
contextRetryCount := 0
|
||||
currentPrompt := cfg.SystemPrompt
|
||||
promptAlreadyAdded := false
|
||||
|
||||
for {
|
||||
// Soft context limit: check and truncate before LLM call
|
||||
@@ -535,12 +557,13 @@ func runTurn(ctx context.Context, al *AgentLoop, ts *turnState, cfg SubTurnConfi
|
||||
|
||||
// Call the agent loop
|
||||
finalContent, err := al.runAgentLoop(ctx, childAgent, processOptions{
|
||||
SessionKey: ts.turnID,
|
||||
UserMessage: currentPrompt,
|
||||
DefaultResponse: "",
|
||||
EnableSummary: false,
|
||||
SendResponse: false,
|
||||
SkipAddUserMessage: promptAlreadyAdded,
|
||||
SessionKey: ts.turnID,
|
||||
UserMessage: currentPrompt,
|
||||
SystemPromptOverride: cfg.ActualSystemPrompt,
|
||||
DefaultResponse: "",
|
||||
EnableSummary: false,
|
||||
SendResponse: false,
|
||||
SkipAddUserMessage: promptAlreadyAdded,
|
||||
})
|
||||
|
||||
// Mark the prompt as added so subsequent truncation retries
|
||||
@@ -600,8 +623,11 @@ func runTurn(ctx context.Context, al *AgentLoop, ts *turnState, cfg SubTurnConfi
|
||||
continue // Retry with recovery prompt
|
||||
}
|
||||
|
||||
// 3. Success - return result
|
||||
return &tools.ToolResult{ForLLM: finalContent}, nil
|
||||
// 3. Success - return result with session history
|
||||
return &tools.ToolResult{
|
||||
ForLLM: finalContent,
|
||||
Messages: childAgent.Sessions.GetHistory(ts.turnID),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -1,6 +1,10 @@
|
||||
package tools
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
// ToolResult represents the structured return value from tool execution.
|
||||
// It provides clear semantics for different types of results and supports
|
||||
@@ -34,6 +38,11 @@ type ToolResult struct {
|
||||
// Media contains media store refs produced by this tool.
|
||||
// When non-empty, the agent will publish these as OutboundMediaMessage.
|
||||
Media []string `json:"media,omitempty"`
|
||||
|
||||
// Messages holds the ephemeral session history after execution.
|
||||
// Only populated by SubTurn executions; used by evaluator_optimizer
|
||||
// to carry stateful worker context across evaluation iterations.
|
||||
Messages []providers.Message `json:"-"`
|
||||
}
|
||||
|
||||
// NewToolResult creates a basic ToolResult with content for the LLM.
|
||||
|
||||
+12
-10
@@ -17,15 +17,17 @@ type SubTurnSpawner interface {
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
|
||||
type SubagentTask struct {
|
||||
@@ -203,7 +205,7 @@ After completing the task, provide a clear summary of what was done.`
|
||||
MaxIterations: maxIter,
|
||||
LLMOptions: llmOptions,
|
||||
}, messages, task.OriginChannel, task.OriginChatID)
|
||||
|
||||
|
||||
if err == nil {
|
||||
result = &ToolResult{
|
||||
ForLLM: fmt.Sprintf(
|
||||
|
||||
Reference in New Issue
Block a user