mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Adds GET /api/tasks/{id}/outcome: finding disposition and severity,
retest verdicts, result-row token totals, tool calls, skill misses, and
wall time in one row. This is the measurement layer an RSI loop needs
before claiming a change helped; it alters no agent behavior.
Metric selection follows harness2 (Google Research, Apache-2.0,
arXiv 2610.14224) and the task-gain / retained-gain / improver-gain
taxonomy in the awesome-rsi list.
120 lines
5.3 KiB
Go
120 lines
5.3 KiB
Go
package db
|
|
|
|
import "context"
|
|
|
|
// The per-task outcome card is the baseline layer for RSI comparisons. It
|
|
// consolidates what a task produced and what it cost so a later harness or
|
|
// lesson change can be judged against a number instead of an impression.
|
|
// Metric selection follows harness2 (Google Research, Apache-2.0,
|
|
// arXiv 2610.14224) and the task-gain / retained-gain / improver-gain taxonomy
|
|
// used by the awesome-rsi list (lobehub).
|
|
|
|
// TaskOutcomeFindings is the finding disposition and severity split.
|
|
type TaskOutcomeFindings struct {
|
|
Total int `json:"total"`
|
|
Confirmed int `json:"confirmed"` // confirmed, resolved, or fixed
|
|
Open int `json:"open"` // pending or in_progress
|
|
FalsePositive int `json:"false_positive"`
|
|
Duplicate int `json:"duplicate"`
|
|
Critical int `json:"critical"` // confirmed only
|
|
High int `json:"high"`
|
|
Medium int `json:"medium"`
|
|
Low int `json:"low"`
|
|
}
|
|
|
|
// TaskOutcomeRetests is the retest verdict split for this task's findings.
|
|
type TaskOutcomeRetests struct {
|
|
Reproduced int `json:"reproduced"`
|
|
Fixed int `json:"fixed"`
|
|
Inconclusive int `json:"inconclusive"`
|
|
}
|
|
|
|
// TaskOutcomeTokens is the task's result-row token total.
|
|
type TaskOutcomeTokens struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
CacheReadTokens int `json:"cache_read_tokens"`
|
|
CacheWriteTokens int `json:"cache_write_tokens"`
|
|
}
|
|
|
|
// TaskOutcome is one task's production and cost baseline.
|
|
type TaskOutcome struct {
|
|
TaskID int64 `json:"task_id"`
|
|
Status string `json:"status"`
|
|
CategoryID int64 `json:"category_id"`
|
|
CompanyID int64 `json:"company_id"`
|
|
Findings TaskOutcomeFindings `json:"findings"`
|
|
Retests TaskOutcomeRetests `json:"retests"`
|
|
Tokens TaskOutcomeTokens `json:"tokens"`
|
|
ToolCalls int `json:"tool_calls"`
|
|
SkillMisses int `json:"skill_misses"`
|
|
WallSeconds int64 `json:"wall_seconds"`
|
|
}
|
|
|
|
// TaskOutcome returns the outcome card for one live task. sql.ErrNoRows means
|
|
// the task does not exist or is deleted.
|
|
func (d *DB) TaskOutcome(ctx context.Context, taskID int64) (TaskOutcome, error) {
|
|
var o TaskOutcome
|
|
err := d.QueryRowContext(ctx, `
|
|
SELECT t.id, t.status,
|
|
COALESCE(t.category_id,0), COALESCE(t.company_id,0),
|
|
COALESCE(f.total,0), COALESCE(f.confirmed,0), COALESCE(f.open,0),
|
|
COALESCE(f.false_positive,0), COALESCE(f.duplicate,0),
|
|
COALESCE(f.critical,0), COALESCE(f.high,0),
|
|
COALESCE(f.medium,0), COALESCE(f.low,0),
|
|
COALESCE(r.reproduced,0), COALESCE(r.fixed,0), COALESCE(r.inconclusive,0),
|
|
COALESCE(a.input_tokens,0), COALESCE(a.output_tokens,0),
|
|
COALESCE(a.cache_read_tokens,0), COALESCE(a.cache_write_tokens,0),
|
|
COALESCE(tu.calls,0), COALESCE(su.misses,0),
|
|
GREATEST(0, EXTRACT(EPOCH FROM
|
|
(COALESCE(t.completed_at, now()) - COALESCE(t.first_run_at, t.created_at)))::bigint)
|
|
FROM tasks t
|
|
LEFT JOIN LATERAL (
|
|
SELECT COUNT(*) AS total,
|
|
COUNT(*) FILTER (WHERE status IN ('confirmed','resolved','fixed')) AS confirmed,
|
|
COUNT(*) FILTER (WHERE status IN ('pending','in_progress')) AS open,
|
|
COUNT(*) FILTER (WHERE status='false_positive') AS false_positive,
|
|
COUNT(*) FILTER (WHERE status='duplicate') AS duplicate,
|
|
COUNT(*) FILTER (WHERE status IN ('confirmed','resolved','fixed') AND severity='critical') AS critical,
|
|
COUNT(*) FILTER (WHERE status IN ('confirmed','resolved','fixed') AND severity='high') AS high,
|
|
COUNT(*) FILTER (WHERE status IN ('confirmed','resolved','fixed') AND severity='medium') AS medium,
|
|
COUNT(*) FILTER (WHERE status IN ('confirmed','resolved','fixed') AND severity='low') AS low
|
|
FROM findings
|
|
WHERE task_id=t.id
|
|
) f ON true
|
|
LEFT JOIN LATERAL (
|
|
SELECT COUNT(*) FILTER (WHERE fr.verdict='reproduced') AS reproduced,
|
|
COUNT(*) FILTER (WHERE fr.verdict='fixed') AS fixed,
|
|
COUNT(*) FILTER (WHERE fr.verdict='inconclusive') AS inconclusive
|
|
FROM finding_retests fr
|
|
JOIN findings f2 ON f2.id=fr.finding_id
|
|
WHERE f2.task_id=t.id
|
|
) r ON true
|
|
LEFT JOIN LATERAL (
|
|
SELECT SUM(input_tokens) AS input_tokens,
|
|
SUM(output_tokens) AS output_tokens,
|
|
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
SUM(cache_write_tokens) AS cache_write_tokens
|
|
FROM activity
|
|
WHERE exploration_id=t.exploration_id AND kind='result'
|
|
) a ON true
|
|
LEFT JOIN LATERAL (
|
|
SELECT COUNT(*) AS calls FROM tool_usage WHERE task_id=t.id
|
|
) tu ON true
|
|
LEFT JOIN LATERAL (
|
|
SELECT COUNT(*) AS misses FROM skill_usage WHERE task_id=t.id AND NOT found
|
|
) su ON true
|
|
WHERE t.id=$1 AND t.deleted_at IS NULL`, taskID).Scan(
|
|
&o.TaskID, &o.Status,
|
|
&o.CategoryID, &o.CompanyID,
|
|
&o.Findings.Total, &o.Findings.Confirmed, &o.Findings.Open,
|
|
&o.Findings.FalsePositive, &o.Findings.Duplicate,
|
|
&o.Findings.Critical, &o.Findings.High, &o.Findings.Medium, &o.Findings.Low,
|
|
&o.Retests.Reproduced, &o.Retests.Fixed, &o.Retests.Inconclusive,
|
|
&o.Tokens.InputTokens, &o.Tokens.OutputTokens,
|
|
&o.Tokens.CacheReadTokens, &o.Tokens.CacheWriteTokens,
|
|
&o.ToolCalls, &o.SkillMisses, &o.WallSeconds,
|
|
)
|
|
return o, err
|
|
}
|