diff --git a/core/taskengine/engine.go b/core/taskengine/engine.go index 8eb6c943..44a48cb5 100644 --- a/core/taskengine/engine.go +++ b/core/taskengine/engine.go @@ -339,23 +339,18 @@ func New(db storage.Storage, config *config.Config, queue *apqueue.Queue, logger // Initialize AI summarizer (global) from aggregator config // Only context-memory API is supported - all email content generation is delegated to context-memory // The aggregator acts as a pass-through for the context-memory response to SendGrid - contextMemorySummarizer := NewContextMemorySummarizerFromAggregatorConfig(config) + contextMemorySummarizer, err := NewContextMemorySummarizerFromAggregatorConfig(config) + if err != nil { + // notifications.summary is enabled but misconfigured — refuse to boot rather than + // silently fall back, so a broken summarizer config surfaces loudly at startup. + logger.Fatal("Invalid notifications.summary configuration — refusing to start", "error", err) + } if contextMemorySummarizer != nil { SetSummarizer(contextMemorySummarizer) logger.Info("AI summarizer initialized", "provider", "context-memory", "base_url", config.NotificationsSummary.APIEndpoint) } else { - // Log why context-memory is not available - if !config.NotificationsSummary.Enabled { - logger.Debug("Context-memory API not available: NotificationsSummary.Enabled is false") - } else if strings.ToLower(config.NotificationsSummary.Provider) != "context-memory" { - logger.Debug("Context-memory API not configured: provider is not 'context-memory'", "provider", config.NotificationsSummary.Provider) - } else if strings.TrimSpace(config.NotificationsSummary.APIEndpoint) == "" { - logger.Debug("Context-memory API not available: api_endpoint not configured in notifications.summary") - } else if strings.TrimSpace(config.NotificationsSummary.APIKey) == "" { - logger.Debug("Context-memory API not available: api_key not configured in notifications.summary") - } - // No summarizer configured - deterministic fallback will be used - logger.Debug("Context-memory API not configured, will use deterministic fallback") + // Summarization not enabled — the deterministic summarizer is used. + logger.Debug("notifications.summary.enabled is false; using deterministic summarizer") } // Initialize global macro variables and secrets from config diff --git a/core/taskengine/summarizer.go b/core/taskengine/summarizer.go index 3290d4cf..23fe2f7a 100644 --- a/core/taskengine/summarizer.go +++ b/core/taskengine/summarizer.go @@ -2,6 +2,7 @@ package taskengine import ( "context" + "fmt" "net/http" "strings" "time" @@ -108,31 +109,34 @@ func ComposeSummarySmart(vm *VM, currentStepName string) Summary { return s } -// NewContextMemorySummarizerFromAggregatorConfig creates a ContextMemorySummarizer from aggregator config -// Reads api_endpoint and api_key from notifications.summary section -func NewContextMemorySummarizerFromAggregatorConfig(c *config.Config) Summarizer { - if c == nil { - return nil - } - if !c.NotificationsSummary.Enabled || strings.ToLower(c.NotificationsSummary.Provider) != "context-memory" { - return nil - } - baseURL := c.NotificationsSummary.APIEndpoint - if strings.TrimSpace(baseURL) == "" { - return nil +// NewContextMemorySummarizerFromAggregatorConfig builds the workflow summarizer from the +// aggregator's notifications.summary config (api_endpoint + api_key). +// +// Returns (nil, nil) when summarization is disabled — a valid "off" mode in which callers +// use the deterministic summarizer. Returns (nil, error) when summarization is ENABLED but +// misconfigured (unsupported provider, or empty endpoint/key) so the daemon fails fast at +// startup instead of silently degrading. There is no hardcoded endpoint default: the origin +// must come from config (avs-infra: ${SUMMARIZER_API_URL} / ${SUMMARIZER_API_KEY}). +func NewContextMemorySummarizerFromAggregatorConfig(c *config.Config) (Summarizer, error) { + if c == nil || !c.NotificationsSummary.Enabled { + return nil, nil // Not enabled — deterministic fallback is used. } - authToken := c.NotificationsSummary.APIKey - if strings.TrimSpace(authToken) == "" { - return nil + if strings.ToLower(c.NotificationsSummary.Provider) != "context-memory" { + return nil, fmt.Errorf("notifications.summary.enabled is true but provider %q is unsupported (expected \"context-memory\")", c.NotificationsSummary.Provider) } + baseURL := strings.TrimSpace(c.NotificationsSummary.APIEndpoint) if baseURL == "" { - baseURL = ContextAPIURL + return nil, fmt.Errorf("notifications.summary.enabled is true but api_endpoint is empty (set SUMMARIZER_API_URL to the Studio origin, e.g. https://app.avaprotocol.org)") + } + authToken := strings.TrimSpace(c.NotificationsSummary.APIKey) + if authToken == "" { + return nil, fmt.Errorf("notifications.summary.enabled is true but api_key is empty (set SUMMARIZER_API_KEY)") } return &ContextMemorySummarizer{ baseURL: baseURL, authToken: authToken, httpClient: &http.Client{Timeout: 30 * time.Second}, - } + }, nil } // FormatForMessageChannels converts a Summary into a concise chat message diff --git a/core/taskengine/summarizer_context_memory.go b/core/taskengine/summarizer_context_memory.go index bf95ea61..78da6fc7 100644 --- a/core/taskengine/summarizer_context_memory.go +++ b/core/taskengine/summarizer_context_memory.go @@ -15,24 +15,20 @@ import ( "github.com/ethereum/go-ethereum/common" ) -const ( - // ContextAPIURL is the production URL for the context-api service - ContextAPIURL = "https://context-api.avaprotocol.org" -) - -// ContextMemorySummarizer implements Summarizer using the context-memory API +// ContextMemorySummarizer implements Summarizer by posting to the workflow-summary +// endpoint (Studio's /api/summarize; the standalone context-memory service is deprecated). type ContextMemorySummarizer struct { baseURL string authToken string httpClient *http.Client } -// NewContextMemorySummarizer creates a new summarizer that calls context-memory API -// baseURL defaults to ContextAPIURL (production) if empty +// NewContextMemorySummarizer creates a summarizer that posts to {baseURL}/api/summarize. +// baseURL MUST be a bare origin (no "/api/summarize" path and no trailing slash) — the +// client appends the path itself; including it would double the path. No default is +// applied: the origin must be supplied explicitly (production wiring fails fast at +// startup when it is missing, see NewContextMemorySummarizerFromAggregatorConfig). func NewContextMemorySummarizer(baseURL, authToken string) Summarizer { - if baseURL == "" { - baseURL = ContextAPIURL - } return &ContextMemorySummarizer{ baseURL: baseURL, authToken: authToken, diff --git a/core/taskengine/summarizer_context_memory_integration_test.go b/core/taskengine/summarizer_context_memory_integration_test.go index b64fcc85..2e466e00 100644 --- a/core/taskengine/summarizer_context_memory_integration_test.go +++ b/core/taskengine/summarizer_context_memory_integration_test.go @@ -73,13 +73,13 @@ type EdgeDefinition struct { Target string `json:"target"` } -// getContextMemoryURL returns the base URL for context-memory API -// Uses CONTEXT_MEMORY_URL env var if set, otherwise defaults to production URL from source code +// getContextMemoryURL returns the base URL for the summarizer API in tests. +// Uses CONTEXT_MEMORY_URL env var if set, otherwise the production origin above. func getContextMemoryURL() string { if url := os.Getenv("CONTEXT_MEMORY_URL"); url != "" { return url } - return ContextAPIURL + return defaultSummarizerURL } // baseURL shared across tests to avoid redeclaration issues diff --git a/core/taskengine/summarizer_format_test.go b/core/taskengine/summarizer_format_test.go index d6398a3f..4a0dd0b4 100644 --- a/core/taskengine/summarizer_format_test.go +++ b/core/taskengine/summarizer_format_test.go @@ -15,6 +15,12 @@ import ( const ( // Default auth token for local context-memory tests ContextMemoryAuthToken = "test-auth-token-12345" + + // defaultSummarizerURL is the production Studio origin, used ONLY as a convenience + // fallback in tests when CONTEXT_MEMORY_URL is unset. Production code has no hardcoded + // default — it reads the origin from notifications.summary.api_endpoint and fails fast + // at startup if it is missing. + defaultSummarizerURL = "https://app.avaprotocol.org" ) type fakeSummarizer struct { @@ -61,7 +67,7 @@ func TestComposeSummarySmart_UsesAISummarizer(t *testing.T) { // Use real context-memory API baseURL := os.Getenv("CONTEXT_MEMORY_URL") if baseURL == "" { - baseURL = ContextAPIURL // Default to production URL from source code + baseURL = defaultSummarizerURL // Test-only fallback when CONTEXT_MEMORY_URL is unset } t.Logf("Using real context-memory API at: %s", baseURL) summarizer := NewContextMemorySummarizer(baseURL, authToken) @@ -523,7 +529,7 @@ func TestComposeSummarySmart_WithRealWorkflowState(t *testing.T) { // Use real context-memory API (defaults to localhost:3000) baseURL := os.Getenv("CONTEXT_MEMORY_URL") if baseURL == "" { - baseURL = ContextAPIURL // Default to production URL from source code + baseURL = defaultSummarizerURL // Test-only fallback when CONTEXT_MEMORY_URL is unset } t.Logf("Using real context-memory API at: %s", baseURL) summarizer := NewContextMemorySummarizer(baseURL, authToken) diff --git a/core/taskengine/vm_runner_rest_sendgrid_integration_test.go b/core/taskengine/vm_runner_rest_sendgrid_integration_test.go index b819967b..b6f6917f 100644 --- a/core/taskengine/vm_runner_rest_sendgrid_integration_test.go +++ b/core/taskengine/vm_runner_rest_sendgrid_integration_test.go @@ -84,7 +84,10 @@ func TestSendGridEmailWithContextMemoryResponse(t *testing.T) { } // Override context_api_endpoint to use mock server mockConfig.MacroSecrets["context_api_endpoint"] = contextMemoryServer.URL - summarizer := NewContextMemorySummarizerFromAggregatorConfig(&mockConfig) + summarizer, err := NewContextMemorySummarizerFromAggregatorConfig(&mockConfig) + if err != nil { + t.Fatalf("Failed to create context-memory summarizer from config: %v", err) + } if summarizer == nil { t.Fatal("Failed to create context-memory summarizer from config") }