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:
parent
c732e63650
commit
53404f18ca
4 changed files with 77 additions and 28 deletions
|
|
@ -64,6 +64,7 @@ type processOptions struct {
|
||||||
SenderID string // Current sender ID for dynamic context
|
SenderID string // Current sender ID for dynamic context
|
||||||
SenderDisplayName string // Current sender display name for dynamic context
|
SenderDisplayName string // Current sender display name for dynamic context
|
||||||
UserMessage string // User message content (may include prefix)
|
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
|
Media []string // media:// refs from inbound message
|
||||||
DefaultResponse string // Response when LLM returns empty
|
DefaultResponse string // Response when LLM returns empty
|
||||||
EnableSummary bool // Whether to trigger summarization
|
EnableSummary bool // Whether to trigger summarization
|
||||||
|
|
@ -1069,6 +1070,17 @@ func (al *AgentLoop) runAgentLoop(
|
||||||
maxMediaSize := cfg.Agents.Defaults.GetMaxMediaSize()
|
maxMediaSize := cfg.Agents.Defaults.GetMaxMediaSize()
|
||||||
messages = resolveMediaRefs(messages, al.mediaStore, maxMediaSize)
|
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
|
// 2. Save user message to session
|
||||||
if !opts.SkipAddUserMessage && opts.UserMessage != "" {
|
if !opts.SkipAddUserMessage && opts.UserMessage != "" {
|
||||||
agent.Sessions.AddMessage(opts.SessionKey, "user", opts.UserMessage)
|
agent.Sessions.AddMessage(opts.SessionKey, "user", opts.UserMessage)
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,14 @@ type SubTurnConfig struct {
|
||||||
// truncated while preserving system messages and recent context.
|
// truncated while preserving system messages and recent context.
|
||||||
MaxContextRunes int
|
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.
|
// 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
|
// Convert tools.SubTurnConfig to agent.SubTurnConfig
|
||||||
agentCfg := SubTurnConfig{
|
agentCfg := SubTurnConfig{
|
||||||
Model: cfg.Model,
|
Model: cfg.Model,
|
||||||
Tools: cfg.Tools,
|
Tools: cfg.Tools,
|
||||||
SystemPrompt: cfg.SystemPrompt,
|
SystemPrompt: cfg.SystemPrompt,
|
||||||
MaxTokens: cfg.MaxTokens,
|
ActualSystemPrompt: cfg.ActualSystemPrompt,
|
||||||
Async: cfg.Async,
|
InitialMessages: cfg.InitialMessages,
|
||||||
Critical: cfg.Critical,
|
MaxTokens: cfg.MaxTokens,
|
||||||
Timeout: cfg.Timeout,
|
Async: cfg.Async,
|
||||||
MaxContextRunes: cfg.MaxContextRunes,
|
Critical: cfg.Critical,
|
||||||
|
Timeout: cfg.Timeout,
|
||||||
|
MaxContextRunes: cfg.MaxContextRunes,
|
||||||
}
|
}
|
||||||
|
|
||||||
return spawnSubTurn(ctx, s.al, parentTS, agentCfg)
|
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
|
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
|
// Resolve MaxContextRunes configuration
|
||||||
maxContextRunes := utils.ResolveMaxContextRunes(cfg.MaxContextRunes, childAgent.ContextWindow)
|
maxContextRunes := utils.ResolveMaxContextRunes(cfg.MaxContextRunes, childAgent.ContextWindow)
|
||||||
|
|
||||||
|
|
@ -501,7 +524,6 @@ func runTurn(ctx context.Context, al *AgentLoop, ts *turnState, cfg SubTurnConfi
|
||||||
truncationRetryCount := 0
|
truncationRetryCount := 0
|
||||||
contextRetryCount := 0
|
contextRetryCount := 0
|
||||||
currentPrompt := cfg.SystemPrompt
|
currentPrompt := cfg.SystemPrompt
|
||||||
promptAlreadyAdded := false
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// Soft context limit: check and truncate before LLM call
|
// 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
|
// Call the agent loop
|
||||||
finalContent, err := al.runAgentLoop(ctx, childAgent, processOptions{
|
finalContent, err := al.runAgentLoop(ctx, childAgent, processOptions{
|
||||||
SessionKey: ts.turnID,
|
SessionKey: ts.turnID,
|
||||||
UserMessage: currentPrompt,
|
UserMessage: currentPrompt,
|
||||||
DefaultResponse: "",
|
SystemPromptOverride: cfg.ActualSystemPrompt,
|
||||||
EnableSummary: false,
|
DefaultResponse: "",
|
||||||
SendResponse: false,
|
EnableSummary: false,
|
||||||
SkipAddUserMessage: promptAlreadyAdded,
|
SendResponse: false,
|
||||||
|
SkipAddUserMessage: promptAlreadyAdded,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mark the prompt as added so subsequent truncation retries
|
// 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
|
continue // Retry with recovery prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Success - return result
|
// 3. Success - return result with session history
|
||||||
return &tools.ToolResult{ForLLM: finalContent}, nil
|
return &tools.ToolResult{
|
||||||
|
ForLLM: finalContent,
|
||||||
|
Messages: childAgent.Sessions.GetHistory(ts.turnID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package tools
|
package tools
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers"
|
||||||
|
)
|
||||||
|
|
||||||
// ToolResult represents the structured return value from tool execution.
|
// ToolResult represents the structured return value from tool execution.
|
||||||
// It provides clear semantics for different types of results and supports
|
// 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.
|
// Media contains media store refs produced by this tool.
|
||||||
// When non-empty, the agent will publish these as OutboundMediaMessage.
|
// When non-empty, the agent will publish these as OutboundMediaMessage.
|
||||||
Media []string `json:"media,omitempty"`
|
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.
|
// NewToolResult creates a basic ToolResult with content for the LLM.
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,17 @@ type SubTurnSpawner interface {
|
||||||
|
|
||||||
// SubTurnConfig holds configuration for spawning a sub-turn.
|
// SubTurnConfig holds configuration for spawning a sub-turn.
|
||||||
type SubTurnConfig struct {
|
type SubTurnConfig struct {
|
||||||
Model string
|
Model string
|
||||||
Tools []Tool
|
Tools []Tool
|
||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
MaxTokens int
|
MaxTokens int
|
||||||
Temperature float64
|
Temperature float64
|
||||||
Async bool // true for async (spawn), false for sync (subagent)
|
Async bool // true for async (spawn), false for sync (subagent)
|
||||||
Critical bool // continue running after parent finishes gracefully
|
Critical bool // continue running after parent finishes gracefully
|
||||||
Timeout time.Duration // 0 = use default (5 minutes)
|
Timeout time.Duration // 0 = use default (5 minutes)
|
||||||
MaxContextRunes int // 0 = auto, -1 = no limit, >0 = explicit limit
|
MaxContextRunes int // 0 = auto, -1 = no limit, >0 = explicit limit
|
||||||
|
ActualSystemPrompt string
|
||||||
|
InitialMessages []providers.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubagentTask struct {
|
type SubagentTask struct {
|
||||||
|
|
@ -203,7 +205,7 @@ After completing the task, provide a clear summary of what was done.`
|
||||||
MaxIterations: maxIter,
|
MaxIterations: maxIter,
|
||||||
LLMOptions: llmOptions,
|
LLMOptions: llmOptions,
|
||||||
}, messages, task.OriginChannel, task.OriginChatID)
|
}, messages, task.OriginChannel, task.OriginChatID)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
result = &ToolResult{
|
result = &ToolResult{
|
||||||
ForLLM: fmt.Sprintf(
|
ForLLM: fmt.Sprintf(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue