diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 8e9a70f2e..e97fb14ff 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -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) diff --git a/pkg/agent/subturn.go b/pkg/agent/subturn.go index b981da399..8e4696142 100644 --- a/pkg/agent/subturn.go +++ b/pkg/agent/subturn.go @@ -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 } } diff --git a/pkg/tools/result.go b/pkg/tools/result.go index cab833284..bf34b7bc6 100644 --- a/pkg/tools/result.go +++ b/pkg/tools/result.go @@ -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. diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index d41cf9a6d..297fb13a5 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -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(