// Package writeback persists turn results as REPORT files. The REPORT is // both the human-facing record and the telemetry trail (model, tier, class, // tokens, rounds) that makes routing decisions auditable. package writeback import ( "fmt" "os" "path/filepath" "strings" "time" "git.knownelement.com/reachableceo/MOPAC/harness/internal/task" ) // Report is everything that lands in a REPORT file. type Report struct { Time time.Time Vertical string Task task.Task Class string Tier string Model string PromptTokens int CompletionTokens int TotalTokens int Rounds int ToolCalls int Denied int Duration time.Duration StopReason string Content string } // Write renders r to /REPORT---.md and // refreshes /REPORT-latest.md. Both writes are atomic (tmp + rename). func Write(dir string, r Report) (string, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return "", fmt.Errorf("writeback: %w", err) } name := fmt.Sprintf("REPORT-%s-%s-%s.md", sanitize(r.Vertical), sanitize(r.Task.ID), r.Time.Format("20060102-150405")) body := r.Render() path := filepath.Join(dir, name) if err := atomicWrite(path, body); err != nil { return "", fmt.Errorf("writeback: %w", err) } if err := atomicWrite(filepath.Join(dir, "REPORT-latest.md"), body); err != nil { return "", fmt.Errorf("writeback: %w", err) } return path, nil } // Render formats the REPORT markdown. func (r Report) Render() string { var b strings.Builder fmt.Fprintf(&b, "# REPORT - %s - %s: %s\n\n", r.Vertical, r.Task.ID, r.Task.Subject) fmt.Fprintf(&b, "- date: %s\n", r.Time.Format(time.RFC3339)) fmt.Fprintf(&b, "- task source: %s\n", r.Task.Source) fmt.Fprintf(&b, "- model: %s (tier %s, class %s)\n", r.Model, r.Tier, r.Class) fmt.Fprintf(&b, "- usage: prompt=%d completion=%d total=%d tokens\n", r.PromptTokens, r.CompletionTokens, r.TotalTokens) fmt.Fprintf(&b, "- turn: rounds=%d tool_calls=%d denied=%d stop=%s\n", r.Rounds, r.ToolCalls, r.Denied, r.StopReason) fmt.Fprintf(&b, "- duration: %s\n", r.Duration.Round(time.Millisecond)) fmt.Fprintf(&b, "\n## Result\n\n") content := r.Content if content == "" { content = "(no content produced)" } b.WriteString(content) b.WriteString("\n") return b.String() } func atomicWrite(path, body string) error { tmp := path + ".tmp" if err := os.WriteFile(tmp, []byte(body), 0o644); err != nil { return err } return os.Rename(tmp, path) } func sanitize(s string) string { var b strings.Builder for _, r := range s { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '_', r == '-': b.WriteRune(r) default: b.WriteByte('-') } } return b.String() }