Add DriftJudge AI role with verdict parsing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 16:45:59 -04:00
parent 7e9d00b80b
commit 83bae00e45
3 changed files with 144 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
package ai
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// Verdict is the drift judge's call on a single window.
type Verdict struct {
OnTask bool
Reason string
}
// ErrInvalidVerdict marks a parseable-but-unusable judge response.
var ErrInvalidVerdict = errors.New("ai: invalid verdict")
type rawVerdict struct {
OnTask bool `json:"on_task"`
Reason string `json:"reason"`
}
// parseVerdict extracts and validates a Verdict from raw CLI output. It reuses
// extractJSON and the shared empty/no-JSON sentinels. A drifting verdict with no
// reason is unusable for the UI and is rejected as ErrInvalidVerdict.
func parseVerdict(s string) (Verdict, error) {
if strings.TrimSpace(s) == "" {
return Verdict{}, ErrEmptyResponse
}
if strings.IndexByte(s, '{') < 0 {
return Verdict{}, ErrNoJSON
}
jsonStr, err := extractJSON(s)
if err != nil {
return Verdict{}, fmt.Errorf("%w: %v", ErrInvalidVerdict, err)
}
var raw rawVerdict
if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
return Verdict{}, fmt.Errorf("%w: %v", ErrInvalidVerdict, err)
}
v := Verdict{OnTask: raw.OnTask, Reason: strings.TrimSpace(raw.Reason)}
if !v.OnTask && v.Reason == "" {
return Verdict{}, ErrInvalidVerdict
}
return v, nil
}