Files
picoclaw/pkg/agent/pipeline_setup.go
T
lxowalle 2992eccbf0 feat: add request-scoped context policies (#2914)
* feat: add request-scoped context policies

Add named turn profiles under agents.defaults so callers can opt into
per-request context and tool policies without changing default chat behavior.

Profiles can disable history, system context, skill prompts, or tools, and can
limit skills/tools with allow lists. Wire profile selection through Pico message
payloads, agent turn execution, Web chat selection, and Web visual config.

Reject invalid turn profiles before saving config through Web APIs and document
the new request context policy behavior.

* fix: address turn profile review blockers

* feat: simplify request context policy config

* fix: suppress tool prompt when turn tools are disabled

* fix: enforce turn profile tool restrictions
2026-05-22 10:06:40 +08:00

120 lines
4.1 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)
contextualSkills := ts.activeSkills
if ts.agent.ContextBuilder != nil {
contextualSkills = ts.agent.ContextBuilder.ResolveActiveSkillsForContext(ts.activeSkills)
}
ts.recordSkillContextSnapshot(skillContextTriggerInitialBuild, contextualSkills)
initialPromptReq := promptBuildRequestForTurn(ts, history, summary, ts.userMessage, ts.media, cfg)
initialPromptReq.ActiveSkills = append([]string(nil), contextualSkills...)
messages := ts.agent.ContextBuilder.BuildMessagesFromPrompt(initialPromptReq)
messages = resolveMediaRefs(messages, p.MediaStore, maxMediaSize)
if !ts.opts.NoHistory {
toolDefs := filterToolsByTurnProfile(ts.agent.Tools.ToProviderDefs(), ts.profile)
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
}
rebuildPromptReq := promptBuildRequestForTurn(ts, history, summary, ts.userMessage, ts.media, cfg)
rebuildPromptReq.ActiveSkills = append([]string(nil), contextualSkills...)
messages = ts.agent.ContextBuilder.BuildMessagesFromPrompt(rebuildPromptReq)
messages = resolveMediaRefs(messages, p.MediaStore, maxMediaSize)
}
}
if !ts.opts.NoHistory && (strings.TrimSpace(ts.userMessage) != "" || len(ts.media) > 0) {
rootMsg := userPromptMessage(ts.userMessage, ts.media)
if len(rootMsg.Media) > 0 {
ts.agent.Sessions.AddFullMessage(ts.sessionKey, rootMsg)
} else {
ts.agent.Sessions.AddMessage(ts.sessionKey, rootMsg.Role, rootMsg.Content)
}
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
}
activeModelName := strings.TrimSpace(ts.agent.Model)
if usedLight {
activeModelName = strings.TrimSpace(sideQuestionModelName(ts.agent, true))
}
activeModelName = resolvedCandidateModelName(activeCandidates, activeModelName)
exec := newTurnExecution(
ts.agent,
ts.opts,
history,
summary,
messages,
)
exec.activeCandidates = activeCandidates
exec.activeModel = activeModel
exec.activeModelConfig = resolveActiveModelConfig(
p.Cfg,
ts.agent.Workspace,
activeCandidates,
activeModel,
p.Cfg.Agents.Defaults.Provider,
)
exec.llmModelName = activeModelName
exec.activeProvider = activeProvider
exec.usedLight = usedLight
return exec, nil
}