diff --git a/.gitignore b/.gitignore index 3317a008c..090909467 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ k8s-*/* tests/**/creds.json ui/simple-saas *.tar +# AI tool config directories (user-specific) +.gemini/ +.opencode/ diff --git a/internal/agent/loop_types.go b/internal/agent/loop_types.go index ecdbaa31e..8a1d20772 100644 --- a/internal/agent/loop_types.go +++ b/internal/agent/loop_types.go @@ -119,6 +119,9 @@ type Loop struct { skillEvolve bool skillNudgeInterval int // nudge every N tool calls (0 = disabled, 15 = default) + // Strip trailing assistant messages before LLM call (for proxy providers) + stripAssistantPrefill bool + // Config permission store for group file writer checks configPermStore store.ConfigPermissionStore @@ -229,6 +232,9 @@ type LoopConfig struct { // Thinking level: "off", "low", "medium", "high" (from agent other_config) ThinkingLevel string + // Strip trailing assistant messages before LLM call (for proxy providers) + StripAssistantPrefill bool + // Self-evolve: predefined agents can update SOUL.md (style/tone) through chat SelfEvolve bool @@ -335,6 +341,7 @@ func NewLoop(cfg LoopConfig) *Loop { selfEvolve: cfg.SelfEvolve, skillEvolve: cfg.SkillEvolve, skillNudgeInterval: cfg.SkillNudgeInterval, + stripAssistantPrefill: cfg.StripAssistantPrefill, configPermStore: cfg.ConfigPermStore, teamStore: cfg.TeamStore, secureCLIStore: cfg.SecureCLIStore, diff --git a/internal/agent/resolver.go b/internal/agent/resolver.go index 803592f84..e414c3c7b 100644 --- a/internal/agent/resolver.go +++ b/internal/agent/resolver.go @@ -347,6 +347,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { SelfEvolve: ag.ParseSelfEvolve(), SkillEvolve: ag.AgentType == store.AgentTypePredefined && ag.ParseSkillEvolve(), SkillNudgeInterval: ag.ParseSkillNudgeInterval(), + StripAssistantPrefill: ag.ParseStripAssistantPrefill(), WorkspaceSharing: ag.ParseWorkspaceSharing(), ShellDenyGroups: ag.ParseShellDenyGroups(), ConfigPermStore: deps.ConfigPermStore, diff --git a/internal/store/agent_store.go b/internal/store/agent_store.go index da1576a99..fa5b83646 100644 --- a/internal/store/agent_store.go +++ b/internal/store/agent_store.go @@ -204,6 +204,21 @@ func (a *AgentData) ParseSkillNudgeInterval() int { return *cfg.SkillNudgeInterval } +// ParseStripAssistantPrefill extracts strip_assistant_prefill from other_config JSONB. +// When true, trailing assistant messages are removed before LLM call (for proxy providers). +func (a *AgentData) ParseStripAssistantPrefill() bool { + if len(a.OtherConfig) == 0 { + return false + } + var cfg struct { + StripAssistantPrefill bool `json:"strip_assistant_prefill"` + } + if json.Unmarshal(a.OtherConfig, &cfg) != nil { + return false + } + return cfg.StripAssistantPrefill +} + // WorkspaceSharingConfig controls per-user workspace isolation. // When shared_dm/shared_group is true, users share the base workspace directory // instead of each getting an isolated subfolder. diff --git a/ui/web/src/i18n/locales/en/agents.json b/ui/web/src/i18n/locales/en/agents.json index 7fa0fe5e3..1bf6fa10e 100644 --- a/ui/web/src/i18n/locales/en/agents.json +++ b/ui/web/src/i18n/locales/en/agents.json @@ -147,7 +147,9 @@ "contextWindow": "Context Window", "contextWindowHint": "Token limit for the model context.", "maxToolIterations": "Max Tool Iterations", - "maxToolIterationsHint": "Max tool calls per request." + "maxToolIterationsHint": "Max tool calls per request.", + "stripAssistantPrefill": "Strip Assistant Prefill", + "stripAssistantPrefillHint": "Remove trailing assistant messages before LLM call. Enable for proxy providers that reject prefill." }, "workspace": { "title": "Workspace", diff --git a/ui/web/src/i18n/locales/vi/agents.json b/ui/web/src/i18n/locales/vi/agents.json index 0e0761951..f2b9bdebd 100644 --- a/ui/web/src/i18n/locales/vi/agents.json +++ b/ui/web/src/i18n/locales/vi/agents.json @@ -147,7 +147,9 @@ "contextWindow": "Cửa sổ ngữ cảnh", "contextWindowHint": "Giới hạn token cho ngữ cảnh model.", "maxToolIterations": "Số lần lặp công cụ tối đa", - "maxToolIterationsHint": "Số lần gọi công cụ tối đa mỗi yêu cầu." + "maxToolIterationsHint": "Số lần gọi công cụ tối đa mỗi yêu cầu.", + "stripAssistantPrefill": "Loại bỏ Assistant Prefill", + "stripAssistantPrefillHint": "Xóa tin nhắn assistant cuối cùng trước khi gọi LLM. Bật cho proxy provider không hỗ trợ prefill." }, "workspace": { "title": "Workspace", diff --git a/ui/web/src/i18n/locales/zh/agents.json b/ui/web/src/i18n/locales/zh/agents.json index 315c4fdf2..ccbe56c38 100644 --- a/ui/web/src/i18n/locales/zh/agents.json +++ b/ui/web/src/i18n/locales/zh/agents.json @@ -147,7 +147,9 @@ "contextWindow": "上下文窗口", "contextWindowHint": "模型上下文的令牌限制。", "maxToolIterations": "最大工具迭代次数", - "maxToolIterationsHint": "每次请求最大工具调用次数。" + "maxToolIterationsHint": "每次请求最大工具调用次数。", + "stripAssistantPrefill": "移除助手预填充", + "stripAssistantPrefillHint": "在调用LLM前移除末尾的助手消息。为不支持预填充的代理提供商启用。" }, "workspace": { "title": "工作区", diff --git a/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx index b717b1820..8d91f4eea 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-overview-tab.tsx @@ -38,6 +38,8 @@ export function AgentOverviewTab({ agent, onUpdate, heartbeat }: AgentOverviewTa const [budgetDollars, setBudgetDollars] = useState( agent.budget_monthly_cents ? String(agent.budget_monthly_cents / 100) : "", ); + const [stripAssistantPrefill, setStripAssistantPrefill] = useState(Boolean(otherCfg.strip_assistant_prefill)); + // Evolution (predefined only) const [selfEvolve, setSelfEvolve] = useState(Boolean(otherCfg.self_evolve)); const [skillEvolve, setSkillEvolve] = useState(Boolean(otherCfg.skill_evolve)); @@ -70,6 +72,7 @@ export function AgentOverviewTab({ agent, onUpdate, heartbeat }: AgentOverviewTa self_evolve: selfEvolve, skill_evolve: skillEvolve, skill_nudge_interval: skillEvolve ? skillNudgeInterval : undefined, + strip_assistant_prefill: stripAssistantPrefill || undefined, }; const budgetCents = budgetDollars ? Math.round(parseFloat(budgetDollars) * 100) : null; await onUpdate({ @@ -128,6 +131,8 @@ export function AgentOverviewTab({ agent, onUpdate, heartbeat }: AgentOverviewTa budgetDollars={budgetDollars} onBudgetDollarsChange={setBudgetDollars} onSaveBlockedChange={setLlmSaveBlocked} + stripAssistantPrefill={stripAssistantPrefill} + onStripAssistantPrefillChange={setStripAssistantPrefill} /> diff --git a/ui/web/src/pages/agents/agent-detail/overview-sections/model-budget-section.tsx b/ui/web/src/pages/agents/agent-detail/overview-sections/model-budget-section.tsx index f0f0f7ab8..27dd48ade 100644 --- a/ui/web/src/pages/agents/agent-detail/overview-sections/model-budget-section.tsx +++ b/ui/web/src/pages/agents/agent-detail/overview-sections/model-budget-section.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { DollarSign } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; import { ProviderModelSelect } from "@/components/shared/provider-model-select"; interface ModelBudgetSectionProps { @@ -19,6 +20,8 @@ interface ModelBudgetSectionProps { budgetDollars: string; onBudgetDollarsChange: (v: string) => void; onSaveBlockedChange?: (blocked: boolean) => void; + stripAssistantPrefill?: boolean; + onStripAssistantPrefillChange?: (v: boolean) => void; } export function ModelBudgetSection({ @@ -28,6 +31,8 @@ export function ModelBudgetSection({ savedProvider, savedModel, budgetDollars, onBudgetDollarsChange, onSaveBlockedChange, + stripAssistantPrefill, + onStripAssistantPrefillChange, }: ModelBudgetSectionProps) { const { t } = useTranslation("agents"); @@ -100,6 +105,20 @@ export function ModelBudgetSection({

{t("general.budgetHint")}

+ + {onStripAssistantPrefillChange && ( +
+
+ +

{t("llmConfig.stripAssistantPrefillHint")}

+
+ +
+ )} ); }