From 10098c479e1b24bf938b17f6797bc442376e4555 Mon Sep 17 00:00:00 2001 From: Will Zimmerman Date: Tue, 23 Jun 2026 00:44:26 -0700 Subject: [PATCH 1/3] feat(summarizer): point summary endpoint at Studio (retire context-memory) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default the workflow-summary base URL to https://app.avaprotocol.org, where Studio now hosts /api/summarize with the identical request/response contract. The Go request/ response structs are unchanged — connecting is a config repoint (NotificationsSummary.APIEndpoint in avs-infra) + rotated APIKey. --- core/taskengine/summarizer_context_memory.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/taskengine/summarizer_context_memory.go b/core/taskengine/summarizer_context_memory.go index bf95ea61..dd92bddc 100644 --- a/core/taskengine/summarizer_context_memory.go +++ b/core/taskengine/summarizer_context_memory.go @@ -16,8 +16,12 @@ import ( ) const ( - // ContextAPIURL is the production URL for the context-api service - ContextAPIURL = "https://context-api.avaprotocol.org" + // ContextAPIURL is the default base URL for the workflow-summary endpoint. + // Migrated from the deprecated context-memory service to Studio, which now hosts + // /api/summarize with the identical request/response contract. The "/api/summarize" + // path is appended by the client. Override per-environment via + // NotificationsSummary.APIEndpoint (avs-infra config) and rotate APIKey. + ContextAPIURL = "https://app.avaprotocol.org" ) // ContextMemorySummarizer implements Summarizer using the context-memory API From e473a0480e2eb70803e5bb5f6b8620a39ea33bb0 Mon Sep 17 00:00:00 2001 From: Will Zimmerman Date: Tue, 23 Jun 2026 01:30:34 -0700 Subject: [PATCH 2/3] docs(summarizer): clarify api_endpoint must be a bare origin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot review on #622 — document that NotificationsSummary.APIEndpoint must be an origin only (client appends /api/summarize); a path or trailing slash would double the URL. Note the legacy constant name. --- core/taskengine/summarizer_context_memory.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/taskengine/summarizer_context_memory.go b/core/taskengine/summarizer_context_memory.go index dd92bddc..8532b935 100644 --- a/core/taskengine/summarizer_context_memory.go +++ b/core/taskengine/summarizer_context_memory.go @@ -16,11 +16,14 @@ import ( ) const ( - // ContextAPIURL is the default base URL for the workflow-summary endpoint. - // Migrated from the deprecated context-memory service to Studio, which now hosts - // /api/summarize with the identical request/response contract. The "/api/summarize" - // path is appended by the client. Override per-environment via - // NotificationsSummary.APIEndpoint (avs-infra config) and rotate APIKey. + // ContextAPIURL is the default base URL (bare origin) for the workflow-summary + // endpoint. Migrated from the deprecated context-memory service to Studio, which now + // hosts /api/summarize with the identical request/response contract. The client + // appends "/api/summarize", so this MUST be an origin only — do NOT include the + // "/api/summarize" path or a trailing slash (either would double the path to + // ".../api/summarize/api/summarize" or "...//api/summarize"). Override per-environment + // via NotificationsSummary.APIEndpoint (avs-infra) and rotate APIKey. NOTE: the + // constant name is legacy — it now points at Studio, not context-memory. ContextAPIURL = "https://app.avaprotocol.org" ) From e6e355dbd1e9fcc4aaf557a779d697660a12e383 Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Tue, 23 Jun 2026 13:59:46 -0700 Subject: [PATCH 3/3] refactor(summarizer): drop hardcoded URL default, fail fast on bad config Remove the ContextAPIURL fallback constant so the summarizer endpoint is never silently defaulted to a baked-in origin. NewContextMemorySummarizer- FromAggregatorConfig now returns (Summarizer, error): (nil, nil) when notifications.summary is disabled, but an error when it is ENABLED yet misconfigured (unsupported provider, or empty api_endpoint/api_key). The engine treats that error as fatal at startup, so a broken summarizer config surfaces loudly instead of degrading to the deterministic path. Tests use a clearly-named test-only defaultSummarizerURL fallback. --- core/taskengine/engine.go | 21 ++++------ core/taskengine/summarizer.go | 38 ++++++++++--------- core/taskengine/summarizer_context_memory.go | 25 ++++-------- ...marizer_context_memory_integration_test.go | 6 +-- core/taskengine/summarizer_format_test.go | 10 ++++- ...m_runner_rest_sendgrid_integration_test.go | 5 ++- 6 files changed, 51 insertions(+), 54 deletions(-) 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 8532b935..78da6fc7 100644 --- a/core/taskengine/summarizer_context_memory.go +++ b/core/taskengine/summarizer_context_memory.go @@ -15,31 +15,20 @@ import ( "github.com/ethereum/go-ethereum/common" ) -const ( - // ContextAPIURL is the default base URL (bare origin) for the workflow-summary - // endpoint. Migrated from the deprecated context-memory service to Studio, which now - // hosts /api/summarize with the identical request/response contract. The client - // appends "/api/summarize", so this MUST be an origin only — do NOT include the - // "/api/summarize" path or a trailing slash (either would double the path to - // ".../api/summarize/api/summarize" or "...//api/summarize"). Override per-environment - // via NotificationsSummary.APIEndpoint (avs-infra) and rotate APIKey. NOTE: the - // constant name is legacy — it now points at Studio, not context-memory. - ContextAPIURL = "https://app.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") }