picoclaw/pkg/agent/action_summary.go
2026-05-09 18:04:32 -07:00

194 lines
4.9 KiB
Go

package agent
import (
"context"
"encoding/json"
"strings"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
)
type TurnActionRecord struct {
Source string `json:"source"`
Tool string `json:"tool,omitempty"`
Text string `json:"text"`
Error bool `json:"error,omitempty"`
}
func appendTurnActionRecord(
records []TurnActionRecord,
source, tool, text string,
isError bool,
) []TurnActionRecord {
text = strings.TrimSpace(text)
if text == "" {
return records
}
rec := TurnActionRecord{
Source: source,
Tool: strings.TrimSpace(tool),
Text: text,
Error: isError,
}
if n := len(records); n > 0 {
prev := records[n-1]
if prev.Source == rec.Source && prev.Tool == rec.Tool && prev.Text == rec.Text && prev.Error == rec.Error {
return records
}
}
return append(records, rec)
}
func finalTurnRenderEligible(al *AgentLoop, exec *turnExecution) bool {
if al == nil || exec == nil {
return false
}
if !al.cfg.Agents.Defaults.UseFinalTurnRender() {
return false
}
return exec.sawSteering
}
func finalTurnRenderModel(ts *turnState, exec *turnExecution) (providers.LLMProvider, string) {
if exec != nil {
if exec.activeProvider != nil && strings.TrimSpace(exec.activeModel) != "" {
return exec.activeProvider, strings.TrimSpace(exec.activeModel)
}
if exec.activeProvider != nil {
return exec.activeProvider, strings.TrimSpace(ts.agent.Model)
}
}
if ts == nil || ts.agent == nil {
return nil, ""
}
return ts.agent.Provider, strings.TrimSpace(ts.agent.Model)
}
func buildFinalTurnRenderInstruction(exec *turnExecution) string {
var b strings.Builder
b.WriteString("Write the final user-facing reply for this already-completed turn.\n")
b.WriteString("Use the same language and general style as the conversation.\n")
b.WriteString("Do not call tools.\n")
b.WriteString("Answer the full accumulated user request across this turn, not only the latest follow-up.\n")
b.WriteString("If a later follow-up clearly corrected, narrowed, or replaced an earlier request, follow the latest clarified intent.\n")
b.WriteString("If later follow-ups added to earlier requests, include the completed additive results together.\n")
b.WriteString("Use only the facts already present in the conversation and tool results. Do not invent missing results.\n")
b.WriteString("Keep the reply concise and natural.\n")
if exec == nil || len(exec.actionLog) == 0 {
return b.String()
}
records := make([]TurnActionRecord, 0, len(exec.actionLog))
for _, rec := range exec.actionLog {
if strings.TrimSpace(rec.Text) == "" {
continue
}
records = append(records, rec)
}
if len(records) == 0 {
return b.String()
}
raw, err := json.MarshalIndent(records, "", " ")
if err != nil {
return b.String()
}
b.WriteString("\nExplicit user-facing outcomes recorded during the turn:\n")
b.WriteString(string(raw))
return b.String()
}
func tryRenderFinalTurnReply(
ctx context.Context,
al *AgentLoop,
ts *turnState,
exec *turnExecution,
fallback string,
) (string, bool) {
fallback = strings.TrimSpace(fallback)
if !finalTurnRenderEligible(al, exec) {
return fallback, false
}
if exec == nil || len(exec.messages) == 0 {
return fallback, false
}
provider, model := finalTurnRenderModel(ts, exec)
if provider == nil || model == "" {
return fallback, false
}
messages := append([]providers.Message(nil), exec.messages...)
instruction := buildFinalTurnRenderInstruction(exec)
messages = append(messages, providers.Message{
Role: "user",
Content: instruction,
})
opts := map[string]any{
"max_tokens": min(ts.agent.MaxTokens, 800),
"temperature": 0.2,
"prompt_cache_key": ts.agent.ID,
}
resp, err := provider.Chat(ctx, messages, nil, model, opts)
if err != nil || resp == nil {
if err != nil {
logger.WarnCF("agent", "Final turn render pass failed", map[string]any{
"agent_id": ts.agent.ID,
"error": err.Error(),
})
}
return fallback, false
}
content := strings.TrimSpace(resp.Content)
if content == "" {
content = strings.TrimSpace(resp.ReasoningContent)
}
if content == "" {
return fallback, false
}
logger.InfoCF("agent", "Rendered final reply from accumulated turn context",
map[string]any{
"agent_id": ts.agent.ID,
"session_key": ts.sessionKey,
"messages_count": len(messages),
"action_record_count": len(exec.actionLog),
})
return content, true
}
func renderFinalTurnReply(
ctx context.Context,
al *AgentLoop,
ts *turnState,
exec *turnExecution,
fallback string,
) string {
content, ok := tryRenderFinalTurnReply(ctx, al, ts, exec, fallback)
if ok {
return content
}
return strings.TrimSpace(fallback)
}
func shouldFinalizeAfterToolLoopWithRender(al *AgentLoop, exec *turnExecution) bool {
if !finalTurnRenderEligible(al, exec) {
return false
}
if exec == nil {
return false
}
return !exec.allResponsesHandled
}
func min(a, b int) int {
if a < b {
return a
}
return b
}