mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Repris du buildAgentNudge d'OpenFox (2.0.133). La passe de vérification partait à chaque fin de tour de build, y compris quand Qwen s'arrêtait sur « ensuite je vais… » ou venait de poser une question avec ask. Chaque passe inutile coûte un prefill complet, évince le cache KV du fil, et produit des « failed » qui ne disent que « pas encore fait » — en courant par-dessus la question restée sans réponse. - Nouveau statut « completed » : le builder marque un critère fait une fois vérifié par lui-même. Seule la vérification marque passed/failed. - Fin de tour avec des critères encore ouverts : le builder est relancé sur ces critères (deux fois au plus) avant toute vérification. - Fin de tour sur une question (ask) : pas de vérification, ni de correction, avant la réponse de l'utilisateur ; idem si le builder pose une question pendant une correction. - Un id de critère écrit entre guillemets (« "2" », « "#2" ») vise bien le bon critère au lieu de #0. - Le panneau des critères montre « completed » (◐). - Test de non-régression de la passe de vérification (code_verify_test.go). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
6.6 KiB
Go
228 lines
6.6 KiB
Go
package loki
|
|
|
|
// code_criteria.go — critères d'acceptation du mode code (contrat d'exécution,
|
|
// repris du session-metadata/criteria d'OpenFox, réécrit en Go — voir
|
|
// NOTICE.md). Les critères sont posés au début d'une tâche (par le modèle via
|
|
// l'outil `criteria`, ou à la main dans l'UI), puis la passe de vérification
|
|
// (code_verify.go) les fait passer à `passed` ou `failed`. Le tour de build ne
|
|
// se termine « vraiment » que quand tout est passed — c'est le contrat.
|
|
//
|
|
// Rangés par discussion dans bbolt (bucket chat, clé crit:<id>) : ils suivent
|
|
// la discussion, sa suppression les emporte.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Criterion struct {
|
|
ID int `json:"id"`
|
|
Text string `json:"text"`
|
|
Status string `json:"status"` // pending | completed (le builder dit « fait ») | passed | failed
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
const critMaxCount = 20
|
|
|
|
func critKey(convID string) string { return "crit:" + convID }
|
|
|
|
func critList(convID string) []Criterion {
|
|
var list []Criterion
|
|
if b, ok := getStoreBytes(bkChat, critKey(convID)); ok && len(b) > 0 {
|
|
_ = json.Unmarshal(b, &list)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func critSave(convID string, list []Criterion) {
|
|
if len(list) == 0 {
|
|
_ = putBytes(bkChat, critKey(convID), nil) // nil supprime la clé
|
|
return
|
|
}
|
|
b, err := json.Marshal(list)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = putStoreBytes(bkChat, critKey(convID), b)
|
|
}
|
|
|
|
func critDrop(convID string) { _ = putBytes(bkChat, critKey(convID), nil) }
|
|
|
|
// critAllPassed : le contrat est rempli. Une liste vide ne compte pas comme
|
|
// remplie — il n'y a simplement pas de contrat.
|
|
func critAllPassed(list []Criterion) bool {
|
|
if len(list) == 0 {
|
|
return false
|
|
}
|
|
for _, c := range list {
|
|
if c.Status != "passed" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func critPending(list []Criterion) int {
|
|
n := 0
|
|
for _, c := range list {
|
|
if c.Status != "passed" {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// critOpen : les critères que le builder n'a pas encore déclarés faits —
|
|
// pending, ou failed et pas encore repris (la reprise les remet à completed).
|
|
func critOpen(list []Criterion) []Criterion {
|
|
var out []Criterion
|
|
for _, c := range list {
|
|
if c.Status == "pending" || c.Status == "failed" || c.Status == "" {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// critRender : la liste formatée pour un prompt (builder ou verifier).
|
|
func critRender(list []Criterion) string {
|
|
var b strings.Builder
|
|
for _, c := range list {
|
|
mark := "[ ]"
|
|
switch c.Status {
|
|
case "passed":
|
|
mark = "[✓]"
|
|
case "failed":
|
|
mark = "[✗]"
|
|
case "completed":
|
|
mark = "[~]" // fait selon le builder, pas encore vérifié
|
|
}
|
|
fmt.Fprintf(&b, "%s #%d %s", mark, c.ID, c.Text)
|
|
if c.Note != "" {
|
|
fmt.Fprintf(&b, " — %s", c.Note)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
func criteriaTool() Tool {
|
|
return Tool{
|
|
Type: "function",
|
|
Function: ToolFunction{
|
|
Name: "criteria",
|
|
Description: "Manage the acceptance criteria of the current task (the contract that defines DONE). " +
|
|
"action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), " +
|
|
"action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"action": map[string]any{"type": "string", "enum": []string{"add", "set", "list", "clear"}},
|
|
"texts": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "add: one entry per criterion, short and testable"},
|
|
"id": map[string]any{"type": "integer", "description": "set: criterion id"},
|
|
"status": map[string]any{"type": "string", "enum": []string{"pending", "completed", "passed", "failed"}},
|
|
"note": map[string]any{"type": "string", "description": "set: why it failed / how it was verified"},
|
|
},
|
|
"required": []string{"action"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// toolCriteria exécute l'outil. allowPass : seul le passage de VÉRIFICATION a
|
|
// le droit de marquer `passed` (sinon le builder s'auto-valide et le contrat ne
|
|
// contraint plus rien).
|
|
func toolCriteria(args map[string]any, allowPass bool) string {
|
|
convID := convEnsureActive()
|
|
action, _ := args["action"].(string)
|
|
list := critList(convID)
|
|
switch action {
|
|
case "list", "":
|
|
if len(list) == 0 {
|
|
return "[aucun critère]"
|
|
}
|
|
return critRender(list)
|
|
case "add":
|
|
raw, _ := args["texts"].([]any)
|
|
next := 1
|
|
for _, c := range list {
|
|
if c.ID >= next {
|
|
next = c.ID + 1
|
|
}
|
|
}
|
|
added := 0
|
|
for _, r := range raw {
|
|
s, ok := r.(string)
|
|
if !ok || strings.TrimSpace(s) == "" {
|
|
continue
|
|
}
|
|
if len(list) >= critMaxCount {
|
|
break
|
|
}
|
|
list = append(list, Criterion{ID: next, Text: strings.TrimSpace(s), Status: "pending"})
|
|
next++
|
|
added++
|
|
}
|
|
if added == 0 {
|
|
return "[erreur] aucun critère fourni (texts)"
|
|
}
|
|
critSave(convID, list)
|
|
critNotify(list)
|
|
return fmt.Sprintf("[ok] %d critère(s) ajouté(s)\n%s", added, critRender(list))
|
|
case "set":
|
|
// id numérique ou chaîne (« "2" », « "#2" ») : les petits modèles
|
|
// écrivent volontiers l'id entre guillemets, qui tombait sinon à #0.
|
|
id := 0
|
|
switch v := args["id"].(type) {
|
|
case float64:
|
|
id = int(v)
|
|
case string:
|
|
id, _ = strconv.Atoi(strings.TrimPrefix(strings.TrimSpace(v), "#"))
|
|
}
|
|
status, _ := args["status"].(string)
|
|
note, _ := args["note"].(string)
|
|
if status == "passed" && !allowPass {
|
|
return "[refusé] Seule la passe de vérification peut marquer un critère passed. Termine ton implémentation ; la vérification suivra."
|
|
}
|
|
for i := range list {
|
|
if list[i].ID != id {
|
|
continue
|
|
}
|
|
if status != "" {
|
|
list[i].Status = status
|
|
}
|
|
if note != "" {
|
|
list[i].Note = strings.TrimSpace(note)
|
|
}
|
|
critSave(convID, list)
|
|
critNotify(list)
|
|
return "[ok] critère #" + fmt.Sprint(id) + " → " + list[i].Status
|
|
}
|
|
return fmt.Sprintf("[erreur] critère #%d inconnu", id)
|
|
case "clear":
|
|
critDrop(convID)
|
|
critNotify(nil)
|
|
return "[ok] critères effacés"
|
|
}
|
|
return "[erreur] action inconnue : " + action
|
|
}
|
|
|
|
// critNotify pousse la liste à l'UI (panneau critères) via le journal de la
|
|
// conversation. Best-effort : hors génération il n'y a pas d'epoch en cours,
|
|
// on publie sur l'epoch courant.
|
|
func critNotify(list []Criterion) {
|
|
if conv == nil {
|
|
return
|
|
}
|
|
conv.mu.Lock()
|
|
epoch := conv.epoch
|
|
conv.mu.Unlock()
|
|
if list == nil {
|
|
list = []Criterion{}
|
|
}
|
|
conv.appendDelta(epoch, map[string]any{"criteria": list, "ts": time.Now().UnixMilli()})
|
|
}
|