diff --git a/cmd/gateway.go b/cmd/gateway.go index 9d587471..44df7e79 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -489,8 +489,18 @@ func runGateway() { } } - // Memory detection + // Memory detection: SQLite (standalone) or PG (managed) — either enables memory. hasMemory := memMgr != nil + if !hasMemory && managedStores != nil && managedStores.Memory != nil { + hasMemory = true + // PG memory is available but SQLite failed or wasn't created. + // Ensure memory tools are registered so wireManagedExtras can wire PG store to them. + if _, exists := toolsReg.Get("memory_search"); !exists { + toolsReg.Register(tools.NewMemorySearchTool(nil)) + toolsReg.Register(tools.NewMemoryGetTool(nil)) + slog.Info("memory tools registered for managed mode (PG-backed)") + } + } // Wire SessionStoreAware + BusAware on tools that need them for _, name := range []string{"sessions_list", "session_status", "sessions_history", "sessions_send"} { diff --git a/cmd/onboard_auto.go b/cmd/onboard_auto.go index 9d55bf18..ef3a3462 100644 --- a/cmd/onboard_auto.go +++ b/cmd/onboard_auto.go @@ -66,7 +66,23 @@ func runAutoOnboard(cfgPath string) bool { fmt.Printf(" Provider: %s (model: %s)\n", provider, cfg.Agents.Defaults.Model) - // 2. Gateway token + // 2. Auto-enable memory: detect embedding-capable API keys from env. + // Embedding providers: openai, openrouter, gemini (same order as resolveEmbeddingProvider). + embProvider := autoDetectEmbeddingProvider(cfg) + if embProvider != "" { + enabled := true + cfg.Agents.Defaults.Memory = &config.MemoryConfig{ + Enabled: &enabled, + EmbeddingProvider: embProvider, + } + fmt.Printf(" Memory: enabled (embedding: %s)\n", embProvider) + } else { + fmt.Println(" Memory: enabled (FTS-only, no embedding API key)") + enabled := true + cfg.Agents.Defaults.Memory = &config.MemoryConfig{Enabled: &enabled} + } + + // 3. Gateway token if cfg.Gateway.Token == "" { cfg.Gateway.Token = onboardGenerateToken(16) slog.Info("auto-onboard: generated gateway token") @@ -150,6 +166,36 @@ func runAutoOnboard(cfgPath string) bool { return true } +// embeddingCapable lists providers that support text embeddings. +// Only these three have embedding provider implementations in resolveEmbeddingProvider. +var embeddingCapable = map[string]bool{ + "openai": true, + "openrouter": true, + "gemini": true, +} + +// autoDetectEmbeddingProvider picks an embedding provider from available API keys. +// Priority: primary provider (GOCLAW_PROVIDER) if embedding-capable, then openai → openrouter → gemini. +func autoDetectEmbeddingProvider(cfg *config.Config) string { + // Prioritize the primary provider if it supports embeddings. + primary := cfg.Agents.Defaults.Provider + if embeddingCapable[primary] && resolveProviderAPIKey(cfg, primary) != "" { + return primary + } + + // Fallback: first available embedding-capable key. + if cfg.Providers.OpenAI.APIKey != "" { + return "openai" + } + if cfg.Providers.OpenRouter.APIKey != "" { + return "openrouter" + } + if cfg.Providers.Gemini.APIKey != "" { + return "gemini" + } + return "" +} + // detectProvider finds the first provider with an API key in the environment. func detectProvider(cfg *config.Config) (string, string) { for _, name := range providerPriority { @@ -234,6 +280,19 @@ func saveCleanConfig(cfgPath string, cfg *config.Config) error { agents["defaults"].(map[string]interface{})["subagents"] = cfg.Agents.Defaults.Subagents } + if mc := cfg.Agents.Defaults.Memory; mc != nil { + mem := map[string]interface{}{ + "enabled": mc.Enabled == nil || *mc.Enabled, + } + if mc.EmbeddingProvider != "" { + mem["embedding_provider"] = mc.EmbeddingProvider + } + if mc.EmbeddingModel != "" { + mem["embedding_model"] = mc.EmbeddingModel + } + agents["defaults"].(map[string]interface{})["memory"] = mem + } + // Build gateway section (no token — secret). gateway := map[string]interface{}{ "host": cfg.Gateway.Host, diff --git a/internal/agent/resolver.go b/internal/agent/resolver.go index c100fbea..95fd901c 100644 --- a/internal/agent/resolver.go +++ b/internal/agent/resolver.go @@ -135,6 +135,15 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { } } + // Per-agent memory: enabled if global memory manager exists AND + // per-agent config doesn't explicitly disable it. + hasMemory := deps.HasMemory + if mc := ag.ParseMemoryConfig(); mc != nil && mc.Enabled != nil { + if !*mc.Enabled { + hasMemory = false + } + } + loop := NewLoop(LoopConfig{ ID: ag.AgentKey, AgentUUID: ag.ID, @@ -150,7 +159,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { ToolPolicy: deps.ToolPolicy, AgentToolPolicy: ag.ParseToolsConfig(), SkillsLoader: deps.Skills, - HasMemory: deps.HasMemory, + HasMemory: hasMemory, ContextFiles: contextFiles, EnsureUserFiles: deps.EnsureUserFiles, ContextFileLoader: deps.ContextFileLoader, diff --git a/internal/http/agents.go b/internal/http/agents.go index fb45ebd2..3509d822 100644 --- a/internal/http/agents.go +++ b/internal/http/agents.go @@ -122,6 +122,14 @@ func (h *AgentsHandler) handleCreate(w http.ResponseWriter, r *http.Request) { } req.RestrictToWorkspace = true + // Default: enable compaction and memory for new agents + if len(req.CompactionConfig) == 0 { + req.CompactionConfig = json.RawMessage(`{}`) + } + if len(req.MemoryConfig) == 0 { + req.MemoryConfig = json.RawMessage(`{"enabled":true}`) + } + // Check if predefined agent has a description for LLM summoning description := extractDescription(req.OtherConfig) if req.AgentType == "predefined" && description != "" && h.summoner != nil { diff --git a/ui/web/src/components/ui/dialog.tsx b/ui/web/src/components/ui/dialog.tsx index 80d7ad60..e6c423d2 100644 --- a/ui/web/src/components/ui/dialog.tsx +++ b/ui/web/src/components/ui/dialog.tsx @@ -51,13 +51,15 @@ function DialogContent({ className, children, showCloseButton = true, + overlayTransparent = false, ...props }: React.ComponentProps & { showCloseButton?: boolean + overlayTransparent?: boolean }) { return ( - + {/* Top row: icon + name + status */}
diff --git a/ui/web/src/pages/agents/agent-create-dialog.tsx b/ui/web/src/pages/agents/agent-create-dialog.tsx index 824c9552..167661f1 100644 --- a/ui/web/src/pages/agents/agent-create-dialog.tsx +++ b/ui/web/src/pages/agents/agent-create-dialog.tsx @@ -33,6 +33,7 @@ interface AgentCreateDialogProps { export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateDialogProps) { const { providers } = useProviders(); const [agentKey, setAgentKey] = useState(""); + const [keyTouched, setKeyTouched] = useState(false); const [displayName, setDisplayName] = useState(""); const [provider, setProvider] = useState(""); const [model, setModel] = useState(""); @@ -63,6 +64,7 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD }); onOpenChange(false); setAgentKey(""); + setKeyTouched(false); setDisplayName(""); setProvider(""); setModel(""); @@ -82,33 +84,44 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD return ( - + Create Agent -
-
- - setAgentKey(slugify(e.target.value))} - placeholder="e.g. my-agent" - /> -

Lowercase letters, numbers, and hyphens only

-
-
- - setDisplayName(e.target.value)} - placeholder="My Agent" - /> +
+
+
+ + setDisplayName(e.target.value)} + onBlur={() => { + if (!keyTouched && displayName.trim()) { + setAgentKey(slugify(displayName.trim())); + } + }} + placeholder="My Agent" + /> +
+
+ + { + setKeyTouched(true); + setAgentKey(e.target.value); + }} + onBlur={() => setAgentKey(slugify(agentKey))} + placeholder="e.g. my-agent" + /> +

Lowercase, numbers, hyphens

+
- + {enabledProviders.length > 0 ? ( setAgentType(v as "open" | "predefined")}> - - - - - Open (per-user context) - Predefined (agent-level) - - +
+ + +
{agentType === "predefined" && (
-
+
{AGENT_PRESETS.map((preset) => ( @@ -185,7 +218,7 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD - diff --git a/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx index e322e968..6ad89af3 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx @@ -149,8 +149,8 @@ export function AgentConfigTab({ agent, onUpdate }: AgentConfigTabProps) { Saved )} -
diff --git a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx index 07b907a0..7f5442d6 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx @@ -1,7 +1,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Card, CardContent } from "@/components/ui/card"; import { ArrowLeft, Bot, Star } from "lucide-react"; import { useAgentDetail } from "../hooks/use-agent-detail"; import { AgentGeneralTab } from "./agent-general-tab"; @@ -88,40 +87,38 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) {
{/* Tabs */} - - - - - General - Config - Files - Shares - +
+ + + General + Config + Files + Shares + - - - + + + - - - + + + - - - + + + - - - - - - + + + + +
); } diff --git a/ui/web/src/pages/agents/agent-detail/agent-files-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-files-tab.tsx index 2b50242c..52e57407 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-files-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-files-tab.tsx @@ -199,9 +199,8 @@ export function AgentFilesTab({ agent, files, onGetFile, onSetFile, onRegenerate size="sm" onClick={handleSave} disabled={!dirty || saving} - className="gap-1" > - + {!saving && } {saving ? "Saving..." : "Save"} )} diff --git a/ui/web/src/pages/agents/agent-detail/agent-general-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-general-tab.tsx index 03692cd6..defa553c 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-general-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-general-tab.tsx @@ -238,8 +238,8 @@ export function AgentGeneralTab({ agent, onUpdate }: AgentGeneralTabProps) { Saved )} -
diff --git a/ui/web/src/pages/agents/summoning-modal.tsx b/ui/web/src/pages/agents/summoning-modal.tsx index fa16d1b7..019945a7 100644 --- a/ui/web/src/pages/agents/summoning-modal.tsx +++ b/ui/web/src/pages/agents/summoning-modal.tsx @@ -72,7 +72,7 @@ export function SummoningModal({ return ( - { if (status === "summoning") e.preventDefault(); }}> + { if (status === "summoning") e.preventDefault(); }}> {status === "completed" @@ -126,7 +126,7 @@ export function SummoningModal({
{/* Agent name */} -

+

{status === "completed" ? ( {agentName} is ready! @@ -148,8 +148,8 @@ export function SummoningModal({ return ( @@ -165,7 +165,7 @@ export function SummoningModal({ {done ? "\u2713" : i + 1}

- + {file.name} {file.label}