// internal/brain/client.go // Package brain provides a lightweight client for querying the ingestion server. // Skill handlers call Query before spawning workers to inject relevant knowledge // from the brain into the task prompt. Errors are suppressed — the brain is // optional context; its absence must never block a skill invocation. package brain import ( "bytes" "context" "encoding/json" "fmt" "io" "log/slog" "net/http" "strings" ) type queryResult struct { Path string `json:"path"` Title string `json:"title"` Excerpt string `json:"excerpt"` Score int `json:"score"` } // Query calls the ingestion server and returns relevant knowledge as a // formatted string ready to prepend to a worker task prompt. // Returns empty string (no error) when baseURL or query is empty, // when the brain is unreachable, or when no results are found. func Query(ctx context.Context, baseURL, query string, limit int) (string, error) { if baseURL == "" || strings.TrimSpace(query) == "" { return "", nil } if limit <= 0 { limit = 3 } body, _ := json.Marshal(map[string]any{"query": query, "limit": limit}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/query", bytes.NewReader(body)) if err != nil { slog.Warn("brain: build request failed", "err", err) return "", nil } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { slog.Warn("brain: ingestion server unreachable", "err", err) return "", nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { slog.Warn("brain: ingestion server returned non-OK", "status", resp.StatusCode) return "", nil } out, _ := io.ReadAll(resp.Body) var result struct { Results []queryResult `json:"results"` } if err := json.Unmarshal(out, &result); err != nil || len(result.Results) == 0 { return "", nil } var b strings.Builder b.WriteString("## Relevant knowledge\n\n") for _, r := range result.Results { title := r.Title if title == "" { title = r.Path } fmt.Fprintf(&b, "### %s\n%s\n\n", title, r.Excerpt) } return b.String(), nil }