mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Extracts candidate lessons from terminal tasks (confirmed, negative, retest caution), gates them on cue + action + evidence, and makes extraction idempotent through a per-source evidence key. ARTEX_RSI_LESSONS selects off / shadow / inject. Shadow logs what would have been shown; inject appends a code-owned, data-not-instructions block after the planner and worker system prompts. No prompt mutation and no adoption: status stays candidate. Credit: the Library admission rule comes from harness2 (Google Research, Apache-2.0, arXiv 2610.14224); the gain taxonomy from awesome-rsi (lobehub).
119 lines
3.4 KiB
Go
119 lines
3.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Autumn-27/artex/agent"
|
|
"github.com/Autumn-27/artex/db"
|
|
)
|
|
|
|
// rsiLessonsMode reads the shadow-mode switch:
|
|
//
|
|
// off (default) no selection, no logging, no prompt change
|
|
// shadow select lessons and log what would have been shown
|
|
// inject select, log, and append the block to planner/worker prompts
|
|
//
|
|
// The flag is read per task-bundle build, so changing it needs a new task or a
|
|
// task-agent invalidation, not a restart.
|
|
func rsiLessonsMode() string {
|
|
switch strings.ToLower(strings.TrimSpace(os.Getenv("ARTEX_RSI_LESSONS"))) {
|
|
case "shadow":
|
|
return "shadow"
|
|
case "inject":
|
|
return "inject"
|
|
default:
|
|
return "off"
|
|
}
|
|
}
|
|
|
|
// wireTaskLessons selects the lessons visible to a task and, in inject mode,
|
|
// attaches the rendered block to the planner and worker. The block is
|
|
// code-owned: it is appended after the rendered prompt and cannot be removed
|
|
// by editing the prompt in the UI. Usage is logged in both modes so a later
|
|
// comparison can tell which tasks saw which lessons.
|
|
func (s *Server) wireTaskLessons(t *Task, wk *agent.Worker, pl *agent.Planner) {
|
|
mode := rsiLessonsMode()
|
|
if mode == "off" || t == nil {
|
|
return
|
|
}
|
|
taskID, err := strconv.ParseInt(t.ID, 10, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
lessons, err := s.m.PG().TaskLessons(context.Background(), taskID, 8)
|
|
if err != nil {
|
|
log.Printf("[rsi] lesson selection failed for task %s: %v", t.ID, err)
|
|
return
|
|
}
|
|
block := db.RenderLessons(lessons, 1800)
|
|
if block == "" {
|
|
return
|
|
}
|
|
if mode == "inject" {
|
|
wk.SetLessonsBlock(block)
|
|
pl.SetLessonsBlock(block)
|
|
}
|
|
ids := make([]int64, 0, len(lessons))
|
|
for _, l := range lessons {
|
|
ids = append(ids, l.ID)
|
|
}
|
|
if err := s.m.PG().LogLessonUsage(context.Background(), taskID, ids, mode); err != nil {
|
|
log.Printf("[rsi] lesson usage log failed for task %s: %v", t.ID, err)
|
|
}
|
|
}
|
|
|
|
// taskLessons returns the lessons visible to one task plus the rendered block.
|
|
// Read-only: it never extracts.
|
|
func (s *Server) taskLessons(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
writeErr(w, 400, "invalid task id")
|
|
return
|
|
}
|
|
lessons, err := s.m.PG().TaskLessons(r.Context(), id, 50)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
writeErr(w, 404, "task not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Printf("[rsi] lesson list failed for task %d: %v", id, err)
|
|
writeErr(w, 500, "lesson query failed")
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{
|
|
"mode": rsiLessonsMode(),
|
|
"lessons": lessons,
|
|
"block": db.RenderLessons(lessons, 1800),
|
|
"count": len(lessons),
|
|
})
|
|
}
|
|
|
|
// extractTaskLessons derives candidate lessons from one task's findings and
|
|
// retests. It is idempotent; call it after the task closes (the manager does
|
|
// this automatically on terminal status) or manually for a backfill.
|
|
func (s *Server) extractTaskLessons(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
writeErr(w, 400, "invalid task id")
|
|
return
|
|
}
|
|
n, err := s.m.PG().ExtractLessonsForTask(r.Context(), id)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
writeErr(w, 404, "task not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Printf("[rsi] lesson extraction failed for task %d: %v", id, err)
|
|
writeErr(w, 500, "lesson extraction failed")
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{"extracted": n})
|
|
}
|