Documentation
¶
Overview ¶
Package evals provides observers and validation helpers for evaluating completed agent traces.
Tracing itself lives in chainguard.dev/driftlessaf/agents/agenttrace; this package layers evaluation on top. An evaluation is an ObservableTraceCallback — a function receiving an Observer and a completed agenttrace.Trace[T] — that reports outcomes through the Observer (Fail, Log, Grade, Increment, Total).
Observers ¶
- NamespacedObserver: hierarchical namespaces ("accuracy", "reliability", ...) built from a factory function
- ResultCollector: wraps another Observer and collects failure messages and Grades for later inspection
- MetricsObserver: publishes evaluation counts, failures, and grades as Prometheus metrics
- testevals.New / testevals.NewPrefix (subpackage): adapt *testing.T to the Observer interface
Validation helpers ¶
Ready-made ObservableTraceCallbacks cover common checks:
// Tool call counts
evals.ExactToolCalls[string](2)
// (also MinimumNToolCalls, RangeToolCalls, NoToolCalls)
// Tool usage constraints
evals.RequiredToolCalls[string]([]string{"search", "analyze"})
evals.OnlyToolCalls[string]("search", "analyze")
// No trace-level errors
evals.NoErrors[string]()
// Custom validation of the trace result
evals.ResultValidator[string](func(result string) error { ... })
Wiring evaluations into a tracer ¶
Inject binds an Observer to an ObservableTraceCallback, producing an agenttrace.TraceCallback that runs when a trace completes:
obs := evals.NewNamespacedObserver(func(name string) evals.Observer {
return customObserver(name)
})
tracer := agenttrace.ByCode[string](
evals.Inject[string](obs.Child("tool-calls"), evals.ExactToolCalls[string](1)),
evals.Inject[string](obs.Child("reliability"), evals.NoErrors[string]()),
)
ctx = agenttrace.WithTracer[string](ctx, tracer)
BuildCallbacks and BuildTracer do the same for a map of named evaluations, namespacing each entry under the observer.
Reporting ¶
The report subpackage turns a NamespacedObserver[*ResultCollector] tree into markdown evaluation reports.
Index ¶
- func BuildCallbacks[T any, O Observer](observer *NamespacedObserver[O], evalMap map[string]ObservableTraceCallback[T]) []agenttrace.TraceCallback[T]
- func BuildTracer[T any, O Observer](observer *NamespacedObserver[O], evalMap map[string]ObservableTraceCallback[T]) agenttrace.Tracer[T]
- func Inject[T any](obs Observer, callback ObservableTraceCallback[T]) agenttrace.TraceCallback[T]
- type Grade
- type MetricsObserver
- type NamespacedObserver
- func (n *NamespacedObserver[T]) Child(name string) *NamespacedObserver[T]
- func (n *NamespacedObserver[T]) Fail(msg string)
- func (n *NamespacedObserver[T]) Grade(score float64, reasoning string)
- func (n *NamespacedObserver[T]) Increment()
- func (n *NamespacedObserver[T]) Log(msg string)
- func (n *NamespacedObserver[T]) Total() int64
- func (n *NamespacedObserver[T]) Walk(visitor func(string, T))
- type ObservableTraceCallback
- func ExactToolCalls[T any](n int) ObservableTraceCallback[T]
- func MinimumNToolCalls[T any](n int) ObservableTraceCallback[T]
- func NoErrors[T any](ignore ...func(error) bool) ObservableTraceCallback[T]
- func NoToolCalls[T any]() ObservableTraceCallback[T]
- func OnlyToolCalls[T any](toolNames ...string) ObservableTraceCallback[T]
- func RangeToolCalls[T any](min, max int) ObservableTraceCallback[T]
- func RequiredToolCalls[T any](toolNames []string) ObservableTraceCallback[T]
- func ResultValidator[T any](validator func(result T) error) ObservableTraceCallback[T]
- type Observer
- type ResultCollector
- func (r *ResultCollector) Fail(msg string)
- func (r *ResultCollector) Failures() []string
- func (r *ResultCollector) Grade(score float64, reasoning string)
- func (r *ResultCollector) Grades() []Grade
- func (r *ResultCollector) Increment()
- func (r *ResultCollector) Log(msg string)
- func (r *ResultCollector) Total() int64
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildCallbacks ¶
func BuildCallbacks[T any, O Observer](observer *NamespacedObserver[O], evalMap map[string]ObservableTraceCallback[T]) []agenttrace.TraceCallback[T]
BuildCallbacks creates a list of TraceCallbacks from a namespaced observer and evaluation map. This helper injects each evaluation function with a child observer to create TraceCallbacks that can be used with ByCode or other tracers.
func BuildTracer ¶
func BuildTracer[T any, O Observer](observer *NamespacedObserver[O], evalMap map[string]ObservableTraceCallback[T]) agenttrace.Tracer[T]
BuildTracer creates a ByCode tracer from a namespaced observer and evaluation map. This helper consolidates the common pattern of setting up comprehensive evaluation tracers by injecting each evaluation function with a child observer and building a ByCode tracer from the resulting callbacks.
func Inject ¶
func Inject[T any](obs Observer, callback ObservableTraceCallback[T]) agenttrace.TraceCallback[T]
Inject creates a TraceCallback by injecting an Observer implementation into an ObservableTraceCallback
Types ¶
type MetricsObserver ¶
type MetricsObserver struct {
// contains filtered or unexported fields
}
MetricsObserver implements Observer interface with Prometheus metrics
func NewMetricsObserver ¶
func NewMetricsObserver[T any](namespace string) *MetricsObserver
NewMetricsObserver creates a metrics observer for the given tracer type and namespace
func (*MetricsObserver) Fail ¶
func (m *MetricsObserver) Fail(msg string)
Fail implements Observer.Fail
func (*MetricsObserver) Grade ¶
func (m *MetricsObserver) Grade(score float64, reasoning string)
Grade implements Observer.Grade
func (*MetricsObserver) Increment ¶
func (m *MetricsObserver) Increment()
Increment implements Observer.Increment
func (*MetricsObserver) Log ¶
func (m *MetricsObserver) Log(msg string)
Log implements Observer.Log (no-op for metrics observer)
func (*MetricsObserver) Total ¶
func (m *MetricsObserver) Total() int64
Total implements Observer.Total
type NamespacedObserver ¶
type NamespacedObserver[T Observer] struct { // contains filtered or unexported fields }
NamespacedObserver provides hierarchical namespacing for Observer instances
func NewNamespacedObserver ¶
func NewNamespacedObserver[T Observer](factory func(string) T) *NamespacedObserver[T]
NewNamespacedObserver creates a new root NamespacedObserver with the given factory function
func (*NamespacedObserver[T]) Child ¶
func (n *NamespacedObserver[T]) Child(name string) *NamespacedObserver[T]
Child returns the child namespace with the given name, creating it if necessary
func (*NamespacedObserver[T]) Fail ¶
func (n *NamespacedObserver[T]) Fail(msg string)
Fail delegates to the inner Observer instance
func (*NamespacedObserver[T]) Grade ¶
func (n *NamespacedObserver[T]) Grade(score float64, reasoning string)
Grade delegates to the inner Observer instance
func (*NamespacedObserver[T]) Increment ¶
func (n *NamespacedObserver[T]) Increment()
Increment delegates to the inner Observer instance
func (*NamespacedObserver[T]) Log ¶
func (n *NamespacedObserver[T]) Log(msg string)
Log delegates to the inner Observer instance
func (*NamespacedObserver[T]) Total ¶
func (n *NamespacedObserver[T]) Total() int64
Total delegates to the inner Observer instance
func (*NamespacedObserver[T]) Walk ¶
func (n *NamespacedObserver[T]) Walk(visitor func(string, T))
Walk traverses the observer tree in depth-first order, calling the visitor function on the current node first, then on all children in sorted order by name
type ObservableTraceCallback ¶
type ObservableTraceCallback[T any] func(Observer, *agenttrace.Trace[T])
ObservableTraceCallback is a function that receives an Observer interface and completed traces
func ExactToolCalls ¶
func ExactToolCalls[T any](n int) ObservableTraceCallback[T]
ExactToolCalls returns an ObservableTraceCallback that validates the trace has exactly n tool calls.
Example ¶
ExampleExactToolCalls shows how to validate exact tool call counts
// Create a mock observer
obs := &mockObserver{}
// Use ExactToolCalls to validate exactly 2 tool calls
evalCallback := evals.ExactToolCalls[string](2)
// Create tracer with the evaluation
tracer := agenttrace.ByCode[string](evals.Inject(obs, evalCallback))
// Create trace with exactly 2 tool calls
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Analyze logs")
// Add exactly 2 tool calls
tc1 := trace.StartToolCall("tc1", "read_logs", nil)
tc1.Complete("log data", nil)
tc2 := trace.StartToolCall("tc2", "analyze", nil)
tc2.Complete("analysis done", nil)
// Complete trace via done callback (triggers evaluation)
done("Analysis complete", nil)
if len(obs.failures) == 0 {
fmt.Println("Validation passed: exactly 2 tool calls")
}
Output: Validation passed: exactly 2 tool calls
func MinimumNToolCalls ¶
func MinimumNToolCalls[T any](n int) ObservableTraceCallback[T]
MinimumNToolCalls returns an ObservableTraceCallback that validates the trace has at least n tool calls.
func NoErrors ¶
func NoErrors[T any](ignore ...func(error) bool) ObservableTraceCallback[T]
NoErrors returns an ObservableTraceCallback that validates no tool calls resulted in errors. Optional ignore functions can be provided to filter out expected errors (e.g., file not found). If any ignore function returns true for a given error, that error is skipped.
A suspended trace (Trace.Suspended) never fails: suspension is an intentional mid-run halt awaiting an out-of-band signal, not a failure, and the resumed run is graded on its own trace.
Example ¶
ExampleNoErrors shows how to validate no errors occurred
// Create a mock observer
obs := &mockObserver{}
evalCallback := evals.NoErrors[string]()
// Create tracer with the evaluation
tracer := agenttrace.ByCode[string](evals.Inject(obs, evalCallback))
// Create successful trace with no errors
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Read and analyze")
// Add successful tool calls
tc1 := trace.StartToolCall("tc1", "read_logs", nil)
tc1.Complete("log content", nil)
tc2 := trace.StartToolCall("tc2", "analyze", nil)
tc2.Complete("analysis complete", nil)
// Complete trace successfully via done callback (triggers evaluation)
done("Processing complete", nil)
if len(obs.failures) == 0 {
fmt.Println("No errors found")
}
Output: No errors found
func NoToolCalls ¶
func NoToolCalls[T any]() ObservableTraceCallback[T]
NoToolCalls returns an ObservableTraceCallback that validates the trace has no tool calls.
func OnlyToolCalls ¶
func OnlyToolCalls[T any](toolNames ...string) ObservableTraceCallback[T]
OnlyToolCalls returns an ObservableTraceCallback that validates the trace only uses the specified tool names.
func RangeToolCalls ¶
func RangeToolCalls[T any](min, max int) ObservableTraceCallback[T]
RangeToolCalls returns an ObservableTraceCallback that validates the trace has between min and max tool calls (inclusive).
func RequiredToolCalls ¶
func RequiredToolCalls[T any](toolNames []string) ObservableTraceCallback[T]
RequiredToolCalls returns an ObservableTraceCallback that validates the trace uses all of the specified tool names at least once.
Example ¶
ExampleRequiredToolCalls shows how to ensure specific tools are called
// Create a mock observer
obs := &mockObserver{}
// Require both read_logs and analyze to be called
evalCallback := evals.RequiredToolCalls[string]([]string{"read_logs", "analyze"})
// Create tracer with the evaluation
tracer := agenttrace.ByCode[string](evals.Inject(obs, evalCallback))
// Create trace and add required tools (plus extra)
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Process data")
// Add required tools
tc1 := trace.StartToolCall("tc1", "read_logs", nil)
tc1.Complete("log data", nil)
tc2 := trace.StartToolCall("tc2", "analyze", nil)
tc2.Complete("analysis done", nil)
// Add extra tool (should be fine)
tc3 := trace.StartToolCall("tc3", "summarize", nil)
tc3.Complete("summary", nil)
// Complete trace via done callback (triggers evaluation)
done("Processing complete", nil)
if len(obs.failures) == 0 {
fmt.Println("All required tools were called")
}
Output: All required tools were called
func ResultValidator ¶
func ResultValidator[T any](validator func(result T) error) ObservableTraceCallback[T]
ResultValidator returns an ObservableTraceCallback that validates the result using a custom validator. The validator is only called if the result is non-nil. T should typically be a pointer type like *MyStruct.
type Observer ¶
type Observer interface {
// Fail marks the evaluation as failed with the given message
// Should be called at most once per Trace evaluation
Fail(string)
// Log logs a message
// Can be called multiple times per Trace evaluation
Log(string)
// Grade assigns a rating (0.0-1.0) with reasoning to the trace result
// Should be called at most once per Trace evaluation
Grade(score float64, reasoning string)
// Increment is called each time a trace is evaluated
Increment()
// Total returns the number of observed instances
Total() int64
}
Observer defines an interface for observing and controlling evaluation execution
type ResultCollector ¶
type ResultCollector struct {
// contains filtered or unexported fields
}
ResultCollector wraps an Observer to collect failure messages and grades
Example ¶
package main
import (
"context"
"fmt"
"sync"
"chainguard.dev/driftlessaf/agents/agenttrace"
"chainguard.dev/driftlessaf/agents/evals"
)
// exampleObserver implements Observer for examples with thread-safety
type exampleObserver struct {
failures []string
logs []string
count int64
mu sync.Mutex
}
func (m *exampleObserver) Fail(msg string) {
m.mu.Lock()
defer m.mu.Unlock()
m.failures = append(m.failures, msg)
}
func (m *exampleObserver) Log(msg string) {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = append(m.logs, msg)
}
func (m *exampleObserver) Grade(score float64, reasoning string) {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = append(m.logs, fmt.Sprintf("Grade: %.2f - %s", score, reasoning))
}
func (m *exampleObserver) Increment() {
m.mu.Lock()
defer m.mu.Unlock()
m.count++
}
func (m *exampleObserver) Total() int64 {
m.mu.Lock()
defer m.mu.Unlock()
return m.count
}
func main() {
// Create a mock observer to demonstrate the pattern
baseObs := &exampleObserver{}
// Wrap it with a result collector
collector := evals.NewResultCollector(baseObs)
// Define an evaluation callback that validates tool calls
callback := func(o evals.Observer, trace *agenttrace.Trace[string]) {
o.Log("Analyzing trace")
if len(trace.ToolCalls) != 1 {
o.Fail("Expected exactly 1 tool call")
}
if trace.Error != nil {
o.Fail("Unexpected error: " + trace.Error.Error())
}
// Give the trace a grade
o.Grade(0.85, "Good tool usage")
}
// Create tracer with the collector
tracer := agenttrace.ByCode[string](evals.Inject(collector, callback))
// Create a trace that will trigger the evaluation
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Process data")
// Add a tool call
tc := trace.StartToolCall("tc1", "data-processor", map[string]any{
"input": "some data",
})
tc.Complete("processed", nil)
// Complete the trace via done callback (this triggers the evaluation)
done("Processing complete", nil)
// Check collected results
failures := collector.Failures()
grades := collector.Grades()
fmt.Printf("Failures: %d\n", len(failures))
fmt.Printf("Grades: %d (score: %.2f)\n", len(grades), grades[0].Score)
}
Output: Failures: 0 Grades: 1 (score: 0.85)
Example (WithNamespacedObserver) ¶
package main
import (
"context"
"errors"
"fmt"
"sync"
"chainguard.dev/driftlessaf/agents/agenttrace"
"chainguard.dev/driftlessaf/agents/evals"
)
// exampleObserver implements Observer for examples with thread-safety
type exampleObserver struct {
failures []string
logs []string
count int64
mu sync.Mutex
}
func (m *exampleObserver) Fail(msg string) {
m.mu.Lock()
defer m.mu.Unlock()
m.failures = append(m.failures, msg)
}
func (m *exampleObserver) Log(msg string) {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = append(m.logs, msg)
}
func (m *exampleObserver) Grade(score float64, reasoning string) {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = append(m.logs, fmt.Sprintf("Grade: %.2f - %s", score, reasoning))
}
func (m *exampleObserver) Increment() {
m.mu.Lock()
defer m.mu.Unlock()
m.count++
}
func (m *exampleObserver) Total() int64 {
m.mu.Lock()
defer m.mu.Unlock()
return m.count
}
func main() {
// Create a namespaced observer using mock observers
namespacedObs := evals.NewNamespacedObserver(func(name string) evals.Observer {
return &exampleObserver{}
})
// Create result collectors for different namespaces
toolCollector := evals.NewResultCollector(namespacedObs.Child("tools"))
errorCollector := evals.NewResultCollector(namespacedObs.Child("errors"))
// Define evaluations for tool calls
toolEval := func(o evals.Observer, trace *agenttrace.Trace[string]) {
for _, tc := range trace.ToolCalls {
if tc.Error != nil {
o.Fail(fmt.Sprintf("Tool %s failed: %v", tc.Name, tc.Error))
}
}
}
// Define evaluations for trace errors
errorEval := func(o evals.Observer, trace *agenttrace.Trace[string]) {
if trace.Error != nil {
o.Fail("Trace error: " + trace.Error.Error())
}
}
// Create tracer with multiple collectors
tracer := agenttrace.ByCode[string](
evals.Inject(toolCollector, toolEval),
evals.Inject(errorCollector, errorEval),
)
// Create a trace with a failing tool call
ctx := agenttrace.WithTracer[string](context.Background(), tracer)
trace, done := agenttrace.StartTrace[string](ctx, "Complex analysis")
tc := trace.StartToolCall("tc1", "analyzer", nil)
tc.Complete(nil, errors.New("analysis failed"))
// Complete the trace via done callback (this triggers both evaluations)
done("Analysis complete", nil)
// Check failures by category
toolFailures := toolCollector.Failures()
errorFailures := errorCollector.Failures()
fmt.Printf("Tool failures: %d\n", len(toolFailures))
fmt.Printf("Error failures: %d\n", len(errorFailures))
}
Output: Tool failures: 1 Error failures: 0
func NewResultCollector ¶
func NewResultCollector(inner Observer) *ResultCollector
NewResultCollector creates a new ResultCollector that wraps the given Observer
func (*ResultCollector) Fail ¶
func (r *ResultCollector) Fail(msg string)
Fail logs the failure message and stores it in the failures list
func (*ResultCollector) Failures ¶
func (r *ResultCollector) Failures() []string
Failures returns a copy of all collected failure messages
func (*ResultCollector) Grade ¶
func (r *ResultCollector) Grade(score float64, reasoning string)
Grade passes through to the inner observer and stores the grade
func (*ResultCollector) Grades ¶
func (r *ResultCollector) Grades() []Grade
Grades returns a copy of all collected grades
func (*ResultCollector) Increment ¶
func (r *ResultCollector) Increment()
Increment passes through to the inner observer
func (*ResultCollector) Log ¶
func (r *ResultCollector) Log(msg string)
Log passes through to the inner observer
func (*ResultCollector) Total ¶
func (r *ResultCollector) Total() int64
Total passes through to the inner observer
Directories
¶
| Path | Synopsis |
|---|---|
|
Package report provides report generation functionality for evaluation results.
|
Package report provides report generation functionality for evaluation results. |
|
Package testevals provides a testing.T adapter for the evals framework.
|
Package testevals provides a testing.T adapter for the evals framework. |