mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-05-25 16:00:35 +00:00
2992eccbf0
* 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
120 lines
4.1 KiB
Go
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
|
|
}
|