Connects the inbound SenderID, which was already carried end-to-end via InboundContext to PromptBuildRequest, into the per-turn user message so historical messages remember who said what. - userPromptMessage gains a senderID parameter. Real human IDs are sanitized via SanitizeMessageName; synthetic triggers (cron, heartbeat, async:*) are filtered out via IsSystemSenderID so they do not surface as distinct users. - pipeline_setup.go now passes ts.opts.Dispatch.SenderID() and uses AddFullMessage unconditionally. The previous AddMessage(role, content) shortcut for media-less turns silently dropped the new Name field, breaking history disambiguation. - context.go (BuildMessagesFromPrompt) propagates req.SenderID onto the current-turn user message that goes to the LLM. After this commit, sessions persist sender attribution and provider adapters receive it on each user message. Adapters that consume Name (OpenAI native field, Anthropic-style content prefix) land in follow-up commits. The existing "## Current Sender" block in buildDynamicContext is unchanged; it continues to describe the latest speaker in the system prompt while Name covers the per-message channel for history. Refs #2702.
100 lines
3.2 KiB
Go
100 lines
3.2 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
)
|
|
|
|
// SetupTurn extracts the one-time initialization phase, returning a
|
|
// turnExecution populated with history, messages, and candidate selection.
|
|
// It replaces lines 56-145 of the original runTurn.
|
|
func (p *Pipeline) SetupTurn(ctx context.Context, ts *turnState) (*turnExecution, error) {
|
|
cfg := p.Cfg
|
|
maxMediaSize := cfg.Agents.Defaults.GetMaxMediaSize()
|
|
|
|
var history []providers.Message
|
|
var summary string
|
|
if !ts.opts.NoHistory {
|
|
if resp, err := p.ContextManager.Assemble(ctx, &AssembleRequest{
|
|
SessionKey: ts.sessionKey,
|
|
Budget: ts.agent.ContextWindow,
|
|
MaxTokens: ts.agent.MaxTokens,
|
|
}); err == nil && resp != nil {
|
|
history = resp.History
|
|
summary = resp.Summary
|
|
}
|
|
}
|
|
ts.captureRestorePoint(history, summary)
|
|
|
|
messages := ts.agent.ContextBuilder.BuildMessagesFromPrompt(
|
|
promptBuildRequestForTurn(ts, history, summary, ts.userMessage, ts.media),
|
|
)
|
|
|
|
messages = resolveMediaRefs(messages, p.MediaStore, maxMediaSize)
|
|
|
|
if !ts.opts.NoHistory {
|
|
toolDefs := ts.agent.Tools.ToProviderDefs()
|
|
if isOverContextBudget(ts.agent.ContextWindow, messages, toolDefs, ts.agent.MaxTokens) {
|
|
logger.WarnCF("agent", "Proactive compression: context budget exceeded before LLM call",
|
|
map[string]any{"session_key": ts.sessionKey})
|
|
if err := p.ContextManager.Compact(ctx, &CompactRequest{
|
|
SessionKey: ts.sessionKey,
|
|
Reason: ContextCompressReasonProactive,
|
|
Budget: ts.agent.ContextWindow,
|
|
}); err != nil {
|
|
logger.WarnCF("agent", "Proactive compact failed", map[string]any{
|
|
"session_key": ts.sessionKey,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
ts.refreshRestorePointFromSession(ts.agent)
|
|
if resp, err := p.ContextManager.Assemble(ctx, &AssembleRequest{
|
|
SessionKey: ts.sessionKey,
|
|
Budget: ts.agent.ContextWindow,
|
|
MaxTokens: ts.agent.MaxTokens,
|
|
}); err == nil && resp != nil {
|
|
history = resp.History
|
|
summary = resp.Summary
|
|
}
|
|
messages = ts.agent.ContextBuilder.BuildMessagesFromPrompt(
|
|
promptBuildRequestForTurn(ts, history, summary, ts.userMessage, ts.media),
|
|
)
|
|
messages = resolveMediaRefs(messages, p.MediaStore, maxMediaSize)
|
|
}
|
|
}
|
|
|
|
if !ts.opts.NoHistory && (strings.TrimSpace(ts.userMessage) != "" || len(ts.media) > 0) {
|
|
rootMsg := userPromptMessage(ts.userMessage, ts.media, ts.opts.Dispatch.SenderID())
|
|
// AddFullMessage preserves all message fields (Name, Media, etc.).
|
|
// AddMessage(role, content) would silently drop Name attribution,
|
|
// breaking multi-user history disambiguation.
|
|
ts.agent.Sessions.AddFullMessage(ts.sessionKey, rootMsg)
|
|
ts.recordPersistedMessage(rootMsg)
|
|
ts.ingestMessage(ctx, p.al, rootMsg)
|
|
}
|
|
|
|
activeCandidates, activeModel, usedLight := p.al.selectCandidates(ts.agent, ts.userMessage, messages)
|
|
activeProvider := ts.agent.Provider
|
|
if usedLight && ts.agent.LightProvider != nil {
|
|
activeProvider = ts.agent.LightProvider
|
|
}
|
|
|
|
exec := newTurnExecution(
|
|
ts.agent,
|
|
ts.opts,
|
|
history,
|
|
summary,
|
|
messages,
|
|
)
|
|
exec.activeCandidates = activeCandidates
|
|
exec.activeModel = activeModel
|
|
exec.activeProvider = activeProvider
|
|
exec.usedLight = usedLight
|
|
|
|
return exec, nil
|
|
}
|