Files
picoclaw/pkg/agent/prompt_turn.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

218 lines
5.9 KiB
Go

package agent
import (
"fmt"
"strings"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers"
)
func promptBuildRequestForTurn(
ts *turnState,
history []providers.Message,
summary string,
currentMessage string,
media []string,
cfg *config.Config,
) PromptBuildRequest {
req := PromptBuildRequest{
History: history,
Summary: summary,
CurrentMessage: currentMessage,
Media: append([]string(nil), media...),
Channel: ts.channel,
ChatID: ts.chatID,
SenderID: ts.opts.Dispatch.SenderID(),
SenderDisplayName: ts.opts.SenderDisplayName,
ActiveSkills: activeSkillNames(ts.agent, ts.opts),
Overlays: promptOverlaysForOptions(ts.opts),
}
hasCallableTools := true
if ts.profile.Enabled {
hasCallableTools = turnProfileHasCallableTools(ts.profile, ts.agent.Tools.ToProviderDefs()) ||
turnProfileNativeSearchCallable(cfg, ts.profile, ts.agent)
}
if turnProfileSystemPromptOff(ts.profile) {
req.SuppressDefaultSystemPrompt = true
req.SuppressSkillContext = true
req.ToolUseFallback = hasCallableTools
}
if ts.profile.Enabled && !hasCallableTools {
req.SuppressToolUseRule = true
}
if turnProfileSkillsOff(ts.profile) {
req.SuppressSkillContext = true
}
if turnProfileCustomSkills(ts.profile) {
req.AllowedSkills = append([]string(nil), ts.profile.AllowedSkills...)
}
if ts.profile.Enabled && ts.profile.ToolsMode == config.TurnProfileModeCustom {
req.AllowedTools = append([]string(nil), ts.profile.AllowedTools...)
}
return req
}
func turnProfileNativeSearchCallable(
cfg *config.Config,
profile config.EffectiveTurnProfile,
agent *AgentInstance,
) bool {
if cfg == nil || agent == nil {
return false
}
if !cfg.Tools.IsToolEnabled("web") || !cfg.Tools.Web.PreferNative {
return false
}
if !turnProfileToolAllowed(profile, "web_search") {
return false
}
nativeProvider, ok := agent.Provider.(providers.NativeSearchCapable)
return ok && nativeProvider.SupportsNativeSearch()
}
func promptBuildRequestForProcessOptions(
agent *AgentInstance,
opts processOptions,
history []providers.Message,
summary string,
currentMessage string,
media []string,
) PromptBuildRequest {
req := PromptBuildRequest{
History: history,
Summary: summary,
CurrentMessage: currentMessage,
Media: append([]string(nil), media...),
Channel: opts.Channel,
ChatID: opts.ChatID,
SenderID: opts.SenderID,
SenderDisplayName: opts.SenderDisplayName,
ActiveSkills: activeSkillNames(agent, opts),
Overlays: promptOverlaysForOptions(opts),
}
profile := opts.TurnProfile
hasCallableTools := true
if profile.Enabled && agent != nil {
hasCallableTools = turnProfileHasCallableTools(profile, agent.Tools.ToProviderDefs())
}
if turnProfileSystemPromptOff(profile) {
req.SuppressDefaultSystemPrompt = true
req.SuppressSkillContext = true
req.ToolUseFallback = hasCallableTools
}
if profile.Enabled && !hasCallableTools {
req.SuppressToolUseRule = true
}
if turnProfileSkillsOff(profile) {
req.SuppressSkillContext = true
}
if turnProfileCustomSkills(profile) {
req.AllowedSkills = append([]string(nil), profile.AllowedSkills...)
}
if profile.Enabled && profile.ToolsMode == config.TurnProfileModeCustom {
req.AllowedTools = append([]string(nil), profile.AllowedTools...)
}
return req
}
func promptOverlaysForOptions(opts processOptions) []PromptPart {
systemPrompt := strings.TrimSpace(opts.SystemPromptOverride)
if systemPrompt == "" {
return nil
}
return []PromptPart{
{
ID: "instruction.subturn_profile",
Layer: PromptLayerInstruction,
Slot: PromptSlotWorkspace,
Source: PromptSource{ID: PromptSourceSubTurnProfile, Name: "subturn.profile"},
Title: "SubTurn System Instructions",
Content: systemPrompt,
Stable: false,
Cache: PromptCacheNone,
},
}
}
func promptContentBlock(part PromptPart, cache *providers.CacheControl) providers.ContentBlock {
if cache == nil {
cache = cacheControlForPromptPart(part)
}
return providers.ContentBlock{
Type: "text",
Text: part.Content,
CacheControl: cache,
PromptLayer: string(part.Layer),
PromptSlot: string(part.Slot),
PromptSource: string(part.Source.ID),
}
}
func cacheControlForPromptPart(part PromptPart) *providers.CacheControl {
switch part.Cache {
case PromptCacheEphemeral:
return &providers.CacheControl{Type: "ephemeral"}
default:
return nil
}
}
func promptMessageWithMetadata(
msg providers.Message,
layer PromptLayer,
slot PromptSlot,
source PromptSourceID,
) providers.Message {
msg.PromptLayer = string(layer)
msg.PromptSlot = string(slot)
msg.PromptSource = string(source)
return msg
}
func promptMessageWithDefaultMetadata(
msg providers.Message,
layer PromptLayer,
slot PromptSlot,
source PromptSourceID,
) providers.Message {
if strings.TrimSpace(msg.PromptSource) != "" {
return msg
}
return promptMessageWithMetadata(msg, layer, slot, source)
}
func userPromptMessage(content string, media []string) providers.Message {
msg := providers.Message{
Role: "user",
Content: content,
}
if len(media) > 0 {
msg.Media = append([]string(nil), media...)
}
return promptMessageWithMetadata(msg, PromptLayerTurn, PromptSlotMessage, PromptSourceUserMessage)
}
func steeringPromptMessage(msg providers.Message) providers.Message {
return promptMessageWithDefaultMetadata(msg, PromptLayerTurn, PromptSlotSteering, PromptSourceSteering)
}
func subTurnResultPromptMessage(content string) providers.Message {
return promptMessageWithMetadata(
providers.Message{Role: "user", Content: fmt.Sprintf("[SubTurn Result] %s", content)},
PromptLayerTurn,
PromptSlotSubTurn,
PromptSourceSubTurnResult,
)
}
func interruptPromptMessage(content string) providers.Message {
return promptMessageWithMetadata(
providers.Message{Role: "user", Content: content},
PromptLayerTurn,
PromptSlotInterrupt,
PromptSourceInterrupt,
)
}