| 1 | package transcript |
| 2 | |
| 3 | import ( |
| 4 | "crypto/sha256" |
| 5 | "fmt" |
| 6 | "log/slog" |
| 7 | ) |
| 8 | |
| 9 | // BaselineError retains the original error for callers and exposes only |
| 10 | // bounded, content-free diagnostics to structured loggers. Record identities |
| 11 | // can originate in imported data, so even those are logged as fingerprints. |
| 12 | type BaselineError struct { |
| 13 | cause error |
| 14 | code string |
| 15 | recordCount int |
| 16 | index int |
| 17 | previousIndex int |
| 18 | record Message |
| 19 | } |
| 20 | |
| 21 | func (e *BaselineError) Error() string { return e.cause.Error() } |
| 22 | func (e *BaselineError) Unwrap() error { return e.cause } |
| 23 | |
| 24 | // Code returns the bounded failure classification used by local diagnostics |
| 25 | // and crash aggregation. It never contains transcript or record data. |
| 26 | func (e *BaselineError) Code() string { return e.code } |
| 27 | |
| 28 | func (e *BaselineError) LogValue() slog.Value { |
| 29 | attrs := []slog.Attr{slog.String("code", e.code), slog.Int("record_count", e.recordCount)} |
| 30 | if e.index >= 0 { |
| 31 | attrs = append(attrs, slog.Int("record_index", e.index), slog.String("role", diagnosticRole(e.record.Role)), |
| 32 | slog.String("record_key", diagnosticKey(e.record.RecordID)), |
| 33 | slog.String("message_key", diagnosticKey(e.record.MessageID)), |
| 34 | slog.String("tool_call_key", diagnosticKey(e.record.ToolCallID))) |
| 35 | } |
| 36 | if e.previousIndex >= 0 { |
| 37 | attrs = append(attrs, slog.Int("previous_record_index", e.previousIndex)) |
| 38 | } |
| 39 | return slog.GroupValue(attrs...) |
| 40 | } |
| 41 | |
| 42 | func newBaselineError(cause error, code string, count, index, previous int, record Message) error { |
| 43 | // Do not retain chat bodies or nested metadata through a returned error. |
| 44 | return &BaselineError{cause: cause, code: code, recordCount: count, index: index, previousIndex: previous, |
| 45 | record: Message{Role: record.Role, RecordID: record.RecordID, MessageID: record.MessageID, ToolCallID: record.ToolCallID}} |
| 46 | } |
| 47 | |
| 48 | func diagnosticKey(identity string) string { |
| 49 | if identity == "" { |
| 50 | return "" |
| 51 | } |
| 52 | return fmt.Sprintf("%x", sha256.Sum256([]byte(identity))) |
| 53 | } |
| 54 | |
| 55 | func diagnosticRole(role string) string { |
| 56 | switch role { |
| 57 | case "system", "user", "assistant", "tool", "notice": |
| 58 | return role |
| 59 | default: |
| 60 | return "other" |
| 61 | } |
| 62 | } |
| 63 |