package routing import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" ) // LogEntry describes a single routing decision to log via the brain MCP. type LogEntry struct { SessionID string Skill string // the original skill the call routed (e.g., "review") Decision string // "local" or "claude" or "claude_fallback" Message string // free-form, e.g. "model=qwen35, pass_rate=0.94" ProjectRoot string DurationMs int64 Failed bool // true → final_status: "fail"; false → "skip" } // Logger posts session_log entries to a brain MCP at BrainURL + /mcp. type Logger struct { BrainURL string HTTP *http.Client } // NewLogger creates a Logger with a 2-second HTTP timeout. func NewLogger(brainURL string) *Logger { return &Logger{ BrainURL: brainURL, HTTP: &http.Client{Timeout: 2 * time.Second}, } } // LogDecision posts a session_log MCP call. Errors are returned but the caller // MUST NOT block real work on them — logging is best-effort. func (l *Logger) LogDecision(ctx context.Context, e LogEntry) error { status := "skip" if e.Failed { status = "fail" } payload := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{ "name": "session_log", "arguments": map[string]any{ "session_id": e.SessionID, "skill": "_routing", "phase": "decide", "final_status": status, "message": fmt.Sprintf("%s: %s — %s", e.Skill, e.Decision, e.Message), "duration_ms": e.DurationMs, "project_root": e.ProjectRoot, }, }, } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("log: marshal: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, l.BrainURL+"/mcp", bytes.NewReader(body)) if err != nil { return fmt.Errorf("log: build request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := l.HTTP.Do(req) if err != nil { return fmt.Errorf("log: request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("log: server returned status %d", resp.StatusCode) } return nil }