mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 17:57:54 -10:00
226 lines
6.0 KiB
Go
226 lines
6.0 KiB
Go
package agenteval
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type BenchmarkInput struct {
|
|
TaskID string
|
|
WorkRoot string
|
|
Models []string
|
|
KeepWorkspaces bool
|
|
// Timeout bounds each task's materialization, agent run, and scoring. A
|
|
// non-positive value leaves the task unbounded.
|
|
Timeout time.Duration
|
|
}
|
|
|
|
type BenchmarkReport struct {
|
|
Contract string `json:"contract"`
|
|
SuiteID string `json:"suiteId"`
|
|
OK bool `json:"ok"`
|
|
Summary BenchmarkSummary `json:"summary"`
|
|
Tasks []BenchmarkTaskReport `json:"tasks"`
|
|
}
|
|
|
|
type BenchmarkTaskReport struct {
|
|
TaskID string `json:"taskId"`
|
|
Model string `json:"model,omitempty"`
|
|
WorkspacePath string `json:"workspacePath"`
|
|
FixturePath string `json:"fixturePath"`
|
|
Agent AgentRunResult `json:"agent"`
|
|
Report Report `json:"report"`
|
|
}
|
|
|
|
type BenchmarkSummary struct {
|
|
TotalTasks int `json:"totalTasks"`
|
|
PassedTasks int `json:"passedTasks"`
|
|
FailedTasks int `json:"failedTasks"`
|
|
BlockedTasks int `json:"blockedTasks"`
|
|
ErrorTasks int `json:"errorTasks"`
|
|
}
|
|
|
|
type Harness struct {
|
|
Materializer Materializer
|
|
Agent AgentRunner
|
|
Runner Runner
|
|
}
|
|
|
|
func (harness Harness) Run(ctx context.Context, suitePath string, suite Suite, input BenchmarkInput) BenchmarkReport {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
report := BenchmarkReport{
|
|
Contract: ReportContractVersion,
|
|
SuiteID: suite.ID,
|
|
}
|
|
tasks, err := selectBenchmarkTasks(suite, input.TaskID)
|
|
if err != nil {
|
|
taskID := input.TaskID
|
|
report.Tasks = append(report.Tasks, BenchmarkTaskReport{
|
|
TaskID: taskID,
|
|
Agent: AgentRunResult{ExitCode: -1, Error: err.Error()},
|
|
Report: Report{
|
|
Contract: ReportContractVersion,
|
|
SuiteID: suite.ID,
|
|
TaskID: taskID,
|
|
Status: StatusError,
|
|
OK: false,
|
|
Summary: Summary{Total: 1, Errors: 1},
|
|
Error: err.Error(),
|
|
Results: []Result{{
|
|
ID: "task",
|
|
Name: "Task selection",
|
|
Kind: ResultChangedFiles,
|
|
Status: StatusError,
|
|
Message: err.Error(),
|
|
}},
|
|
},
|
|
})
|
|
report.finishSummary()
|
|
return report
|
|
}
|
|
|
|
for _, task := range tasks {
|
|
for _, model := range benchmarkModels(input.Models) {
|
|
report.Tasks = append(report.Tasks, harness.runTask(ctx, suitePath, suite, task, model, input))
|
|
}
|
|
}
|
|
report.finishSummary()
|
|
return report
|
|
}
|
|
|
|
func (harness Harness) runTask(ctx context.Context, suitePath string, suite Suite, task Task, model string, input BenchmarkInput) BenchmarkTaskReport {
|
|
if input.Timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, input.Timeout)
|
|
defer cancel()
|
|
}
|
|
taskReport := BenchmarkTaskReport{
|
|
TaskID: task.ID,
|
|
Model: model,
|
|
Agent: AgentRunResult{ExitCode: -1},
|
|
}
|
|
if harness.Agent == nil {
|
|
taskReport.Agent = AgentRunResult{ExitCode: -1, Error: "agent command is required"}
|
|
taskReport.Report = Score(suite, ScoreInput{
|
|
TaskID: task.ID,
|
|
Blocked: true,
|
|
BlockReason: taskReport.Agent.Error,
|
|
})
|
|
return taskReport
|
|
}
|
|
|
|
workspace, err := harness.Materializer.MaterializeTask(ctx, suitePath, task, MaterializeInput{
|
|
WorkRoot: input.WorkRoot,
|
|
})
|
|
if err != nil {
|
|
taskReport.Agent.Error = err.Error()
|
|
taskReport.Report = errorReport(suite.ID, task.ID, fmt.Sprintf("workspace materialization failed: %v", err))
|
|
return taskReport
|
|
}
|
|
taskReport.WorkspacePath = workspace.Path
|
|
taskReport.FixturePath = workspace.FixturePath
|
|
if !input.KeepWorkspaces {
|
|
defer func() { _ = os.RemoveAll(workspace.Path) }()
|
|
}
|
|
|
|
agentResult := harness.Agent.Run(ctx, AgentRunInput{
|
|
TaskID: task.ID,
|
|
Model: model,
|
|
Prompt: task.Prompt,
|
|
WorkspacePath: workspace.Path,
|
|
})
|
|
taskReport.Agent = agentResult
|
|
if agentResult.Error != "" || agentResult.ExitCode != 0 {
|
|
reason := firstNonEmpty(agentResult.Error, strings.TrimSpace(agentResult.Stderr), fmt.Sprintf("agent exited with code %d", agentResult.ExitCode))
|
|
taskReport.Report = Score(suite, ScoreInput{
|
|
TaskID: task.ID,
|
|
Blocked: true,
|
|
BlockReason: reason,
|
|
})
|
|
return taskReport
|
|
}
|
|
|
|
taskReport.Report = harness.Runner.Run(ctx, suite, RunInput{
|
|
TaskID: task.ID,
|
|
WorkspacePath: workspace.Path,
|
|
TraceStdout: agentResult.Stdout,
|
|
})
|
|
return taskReport
|
|
}
|
|
|
|
func (report *BenchmarkReport) finishSummary() {
|
|
report.Summary = BenchmarkSummary{TotalTasks: len(report.Tasks)}
|
|
for _, task := range report.Tasks {
|
|
switch {
|
|
case task.Report.OK:
|
|
report.Summary.PassedTasks++
|
|
case task.Report.Status == StatusBlocked:
|
|
report.Summary.BlockedTasks++
|
|
case task.Report.Status == StatusError:
|
|
report.Summary.ErrorTasks++
|
|
default:
|
|
report.Summary.FailedTasks++
|
|
}
|
|
}
|
|
report.OK = report.Summary.TotalTasks > 0 &&
|
|
report.Summary.FailedTasks == 0 &&
|
|
report.Summary.BlockedTasks == 0 &&
|
|
report.Summary.ErrorTasks == 0
|
|
}
|
|
|
|
func selectBenchmarkTasks(suite Suite, taskID string) ([]Task, error) {
|
|
if taskID == "" {
|
|
tasks := make([]Task, 0, len(suite.Tasks))
|
|
for _, task := range suite.Tasks {
|
|
tasks = append(tasks, normalizeTask(task))
|
|
}
|
|
return tasks, nil
|
|
}
|
|
task, err := selectTask(suite, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []Task{task}, nil
|
|
}
|
|
|
|
func benchmarkModels(models []string) []string {
|
|
normalized := make([]string, 0, len(models))
|
|
seen := map[string]bool{}
|
|
for _, model := range models {
|
|
model = strings.TrimSpace(model)
|
|
if model == "" || seen[model] {
|
|
continue
|
|
}
|
|
seen[model] = true
|
|
normalized = append(normalized, model)
|
|
}
|
|
if len(normalized) == 0 {
|
|
return []string{""}
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func errorReport(suiteID string, taskID string, message string) Report {
|
|
return Report{
|
|
Contract: ReportContractVersion,
|
|
SuiteID: suiteID,
|
|
TaskID: taskID,
|
|
Status: StatusError,
|
|
OK: false,
|
|
Summary: Summary{Total: 1, Errors: 1},
|
|
Error: message,
|
|
Results: []Result{{
|
|
ID: "benchmark",
|
|
Name: "Benchmark harness",
|
|
Kind: ResultChangedFiles,
|
|
Status: StatusError,
|
|
Message: message,
|
|
}},
|
|
}
|
|
}
|