package doctor import ( "encoding/json" "fmt" "os" "strings" "reasonix/internal/agent" "reasonix/internal/memorycompiler" "reasonix/internal/provider" ) const qualityReportSchemaVersion = 1 // QualityOptions selects one persisted session and produces a content-free // diagnostic summary suitable for a public issue or discussion. type QualityOptions struct { Version string SessionRef string } type QualityReport struct { SchemaVersion int `json:"schema_version"` Version string `json:"version"` Profile QualityProfile `json:"profile"` Transcript QualityTranscript `json:"transcript"` Usage QualityUsage `json:"usage"` Signals QualitySignals `json:"signals"` Warnings []string `json:"warnings,omitempty"` } type QualityProfile struct { ModelFamily string `json:"model_family"` RuntimeProfile string `json:"runtime_profile"` CollaborationMode string `json:"collaboration_mode"` ToolApprovalMode string `json:"tool_approval_mode"` GoalActive bool `json:"goal_active"` Recovered bool `json:"recovered"` } type QualityTranscript struct { Messages int `json:"messages"` UserMessages int `json:"user_messages"` AssistantMessages int `json:"assistant_messages"` ToolResults int `json:"tool_results"` ToolCalls int `json:"tool_calls"` WriterCalls int `json:"writer_calls"` VerificationCalls int `json:"verification_calls"` ToolCallTurnsWithoutReasoning int `json:"tool_call_turns_without_reasoning"` CompactionSummaries int `json:"compaction_summaries"` } type QualityUsage struct { Available bool `json:"available"` Requests int `json:"requests"` PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` ReasoningTokens int `json:"reasoning_tokens"` CacheHitTokens int `json:"cache_hit_tokens"` CacheMissTokens int `json:"cache_miss_tokens"` CacheHitPercent *int `json:"cache_hit_percent,omitempty"` } type QualitySignals struct { ExecutorRequests int `json:"executor_requests"` PlannerRequests int `json:"planner_requests"` SubagentRequests int `json:"subagent_requests"` CompactionRequests int `json:"compaction_requests"` ClassifierRequests int `json:"classifier_requests"` OtherRequests int `json:"other_requests"` } type qualityTelemetry struct { Version int `json:"version"` Usage struct { PromptTokens int `json:"promptTokens"` CompletionTokens int `json:"completionTokens"` ReasoningTokens int `json:"reasoningTokens"` CacheHitTokens int `json:"cacheHitTokens"` CacheMissTokens int `json:"cacheMissTokens"` RequestCount int `json:"requestCount"` Sources map[string]qualitySourceUsage `json:"sources"` } `json:"usage"` } type qualitySourceUsage struct { RequestCount int `json:"requestCount"` } // CollectQuality reads a session without returning any transcript text, path, // identifier, tool argument/output, custom model name, or provider endpoint. func CollectQuality(opts QualityOptions) (QualityReport, error) { path, err := resolveSessionBundlePath(strings.TrimSpace(opts.SessionRef)) if err != nil { return QualityReport{}, err } session, err := agent.LoadSession(path) if err != nil { return QualityReport{}, err } meta, _, err := agent.LoadBranchMeta(path) if err != nil { return QualityReport{}, err } report := QualityReport{ SchemaVersion: qualityReportSchemaVersion, Version: strings.TrimSpace(opts.Version), Profile: QualityProfile{ ModelFamily: publicModelFamily(meta.Model), RuntimeProfile: publicTokenMode(meta.TokenMode), CollaborationMode: publicCollaborationMode(meta.Mode, meta.Goal), ToolApprovalMode: publicToolApprovalMode(meta.Mode, meta.ToolApprovalMode), GoalActive: strings.TrimSpace(meta.Goal) != "", Recovered: meta.Recovered, }, } report.Transcript = summarizeQualityTranscript(session.Snapshot()) if telemetry, ok := loadQualityTelemetry(path + ".telemetry.json"); ok { report.Usage = summarizeQualityUsage(telemetry) report.Signals = summarizeQualitySignals(telemetry.Usage.Sources, telemetry.Usage.RequestCount) } else { report.Warnings = append(report.Warnings, "desktop telemetry is unavailable for this session") } if report.Profile.ModelFamily == "deepseek" && report.Transcript.ToolCallTurnsWithoutReasoning > 0 { report.Warnings = append(report.Warnings, "one or more DeepSeek tool-call turns have no persisted reasoning content") } if report.Transcript.WriterCalls > 0 && report.Transcript.VerificationCalls == 0 { report.Warnings = append(report.Warnings, "writer tools were used without a persisted verification-shaped shell call") } return report, nil } func summarizeQualityTranscript(messages []provider.Message) QualityTranscript { var out QualityTranscript out.Messages = len(messages) for _, message := range messages { switch message.Role { case provider.RoleUser: out.UserMessages++ if agent.IsCompactionSummary(message) { out.CompactionSummaries++ } case provider.RoleAssistant: out.AssistantMessages++ if len(message.ToolCalls) > 0 && strings.TrimSpace(message.ReasoningContent) == "" { out.ToolCallTurnsWithoutReasoning++ } for _, call := range message.ToolCalls { out.ToolCalls++ if qualityWriterTool(call.Name) { out.WriterCalls++ } if memorycompiler.IsVerificationToolCall(call.Name, call.Arguments) { out.VerificationCalls++ } } case provider.RoleTool: out.ToolResults++ } } return out } func qualityWriterTool(name string) bool { switch strings.ToLower(strings.TrimSpace(name)) { case "edit_file", "multi_edit", "write_file", "move_file", "delete_range", "delete_symbol", "notebook_edit": return true default: return false } } func loadQualityTelemetry(path string) (qualityTelemetry, bool) { data, err := os.ReadFile(path) if err != nil { return qualityTelemetry{}, false } var telemetry qualityTelemetry if json.Unmarshal(data, &telemetry) != nil || telemetry.Version <= 0 { return qualityTelemetry{}, false } return telemetry, true } func summarizeQualityUsage(telemetry qualityTelemetry) QualityUsage { usage := QualityUsage{ Available: true, Requests: telemetry.Usage.RequestCount, PromptTokens: telemetry.Usage.PromptTokens, CompletionTokens: telemetry.Usage.CompletionTokens, ReasoningTokens: telemetry.Usage.ReasoningTokens, CacheHitTokens: telemetry.Usage.CacheHitTokens, CacheMissTokens: telemetry.Usage.CacheMissTokens, } if total := usage.CacheHitTokens + usage.CacheMissTokens; total > 0 { percent := usage.CacheHitTokens * 100 / total usage.CacheHitPercent = &percent } return usage } func summarizeQualitySignals(sources map[string]qualitySourceUsage, total int) QualitySignals { var out QualitySignals for source, usage := range sources { switch strings.ToLower(strings.TrimSpace(source)) { case "", "executor": out.ExecutorRequests += usage.RequestCount case "planner": out.PlannerRequests += usage.RequestCount case "subagent": out.SubagentRequests += usage.RequestCount case "compaction": out.CompactionRequests += usage.RequestCount case "classifier": out.ClassifierRequests += usage.RequestCount default: out.OtherRequests += usage.RequestCount } } accounted := out.ExecutorRequests + out.PlannerRequests + out.SubagentRequests + out.CompactionRequests + out.ClassifierRequests + out.OtherRequests if total > accounted { out.OtherRequests += total - accounted } return out } func publicModelFamily(model string) string { model = strings.ToLower(strings.TrimSpace(model)) switch { case model == "": return "unknown" case strings.Contains(model, "deepseek"): return "deepseek" case strings.Contains(model, "claude"), strings.Contains(model, "anthropic"): return "anthropic" case strings.Contains(model, "openai"), strings.Contains(model, "gpt"), strings.Contains(model, "codex"): return "openai" case strings.Contains(model, "glm"), strings.Contains(model, "zhipu"): return "zhipu/glm" case strings.Contains(model, "kimi"), strings.Contains(model, "moonshot"): return "kimi/moonshot" case strings.Contains(model, "minimax"): return "minimax" case strings.Contains(model, "qwen"), strings.Contains(model, "dashscope"): return "qwen" case strings.Contains(model, "mimo"): return "mimo" case strings.Contains(model, "gemini"), strings.Contains(model, "google"): return "google/gemini" case strings.Contains(model, "ollama"): return "ollama/local" default: return "custom/unknown" } } func publicTokenMode(mode string) string { switch strings.ToLower(strings.TrimSpace(mode)) { case "economy": return "economy" case "delivery": return "delivery" default: return "balanced" } } func publicCollaborationMode(mode, goal string) string { if strings.Contains(strings.ToLower(strings.TrimSpace(mode)), "plan") { return "plan" } if strings.TrimSpace(goal) != "" { return "goal" } return "normal" } func publicToolApprovalMode(mode, approval string) string { switch strings.ToLower(strings.TrimSpace(approval)) { case "auto": return "auto" case "yolo": return "yolo" } if strings.Contains(strings.ToLower(strings.TrimSpace(mode)), "yolo") { return "yolo" } return "ask" } func RenderQualityText(report QualityReport) string { cache := "n/a" if report.Usage.CacheHitPercent != nil { cache = fmt.Sprintf("%d%%", *report.Usage.CacheHitPercent) } var out strings.Builder fmt.Fprintf(&out, "Reasonix quality diagnostics\n") fmt.Fprintf(&out, "- version: %s\n", valueOrUnknown(report.Version)) fmt.Fprintf(&out, "- model family: %s\n", report.Profile.ModelFamily) fmt.Fprintf(&out, "- profile: runtime=%s collaboration=%s approval=%s goal=%t recovered=%t\n", report.Profile.RuntimeProfile, report.Profile.CollaborationMode, report.Profile.ToolApprovalMode, report.Profile.GoalActive, report.Profile.Recovered) fmt.Fprintf(&out, "- transcript: messages=%d user=%d assistant=%d tool_results=%d tool_calls=%d writers=%d verification=%d compaction_summaries=%d\n", report.Transcript.Messages, report.Transcript.UserMessages, report.Transcript.AssistantMessages, report.Transcript.ToolResults, report.Transcript.ToolCalls, report.Transcript.WriterCalls, report.Transcript.VerificationCalls, report.Transcript.CompactionSummaries) fmt.Fprintf(&out, "- reasoning: tool_call_turns_without_reasoning=%d\n", report.Transcript.ToolCallTurnsWithoutReasoning) if report.Usage.Available { fmt.Fprintf(&out, "- usage: requests=%d prompt=%d completion=%d reasoning=%d cache_hit=%s\n", report.Usage.Requests, report.Usage.PromptTokens, report.Usage.CompletionTokens, report.Usage.ReasoningTokens, cache) fmt.Fprintf(&out, "- request sources: executor=%d planner=%d subagent=%d compaction=%d classifier=%d other=%d\n", report.Signals.ExecutorRequests, report.Signals.PlannerRequests, report.Signals.SubagentRequests, report.Signals.CompactionRequests, report.Signals.ClassifierRequests, report.Signals.OtherRequests) } else { fmt.Fprintf(&out, "- usage: unavailable\n") } for _, warning := range report.Warnings { fmt.Fprintf(&out, "- warning: %s\n", warning) } fmt.Fprintf(&out, "- privacy: transcript text, paths, session identifiers, tool arguments/output, endpoints, and custom model names are omitted\n") return out.String() } func valueOrUnknown(value string) string { if value = strings.TrimSpace(value); value != "" { return value } return "unknown" }