Skip to main content
Aggregates compute summary tables across all sessions in a view. Use them for pass rate summaries, average metrics, or any cross-session analysis.

Register an aggregate

app.dashboard.aggregate('eval-summary', {
  collect: ({ evalResults }) => {
    const result = {};
    for (const [name, r] of Object.entries(evalResults)) {
      result[`${name}_pass`] = r.pass;
      result[`${name}_score`] = r.score ?? 0;
    }
    return result;
  },
  reduce: (collected) => {
    const evalNames = new Set();
    for (const s of collected) {
      for (const key of Object.keys(s.values)) {
        if (key.endsWith('_pass')) evalNames.add(key.replace('_pass', ''));
      }
    }
    return Array.from(evalNames).map(name => ({
      'Eval': name,
      'Pass Rate': +(collected.filter(s => s.values[`${name}_pass`]).length / collected.length).toFixed(2),
      'Avg Score': +(collected.reduce((sum, s) => {
        const v = s.values[`${name}_score`];
        return sum + (typeof v === 'number' ? v : 0);
      }, 0) / collected.length).toFixed(2),
    }));
  },
}, { label: 'Eval Pass Rates' });

How it works

Aggregates use a { collect, reduce } pattern:
  1. collect runs per session and returns a flat Record<string, boolean | number | string>
  2. reduce receives all collected sessions and returns an array of table rows
The aggregate table is rendered on the dashboard view with columns auto-derived from the row keys.

collect context

The collect function receives an extended AggregateContext:
FieldTypeDescription
entriesobject[]Raw JSONL entries for the session
statsEvalLogStatsComputed session stats
projectNamestringProject name
sessionIdstringSession ID
sourcestring"session" or "agent-{id}"
evalResultsRecord<string, { pass, score, error?, message? }>Cached eval results
enrichResultsRecord<string, Record<string, value>>Cached enrichment results
filterValuesRecord<string, boolean | number | string>Computed filter values

Options

OptionTypeDescription
labelstringHuman-readable section label (defaults to the name)
conditionConditionFunctionOptional per-session gate

Aggregates on named views

Use .aggregate() on the view builder to scope an aggregate to a specific view:
app.dashboard.view('quality', { label: 'Quality' })
  .aggregate('session-metrics', {
    collect: ({ stats }) => ({
      turnCount: stats.turnCount,
      toolCalls: stats.toolCallCount,
    }),
    reduce: (collected) => {
      const n = collected.length || 1;
      let turns = 0, tools = 0;
      for (const s of collected) {
        turns += typeof s.values.turnCount === 'number' ? s.values.turnCount : 0;
        tools += typeof s.values.toolCalls === 'number' ? s.values.toolCalls : 0;
      }
      return [
        { Metric: 'Avg Turns', Value: +(turns / n).toFixed(1) },
        { Metric: 'Avg Tool Calls', Value: +(tools / n).toFixed(1) },
      ];
    },
  })
  .filter('turns', ({ stats }) => stats.turnCount, { label: 'Turns' });

reduce input

The reduce function receives an array of CollectedSession objects:
interface CollectedSession {
  projectName: string;
  sessionId: string;
  values: Record<string, boolean | number | string>;
}
Return an array of objects where each object becomes a table row. Column names are derived from the object keys.

Conditional aggregates

Use the condition option to skip a session during collection:
app.dashboard.aggregate('tool-stats', {
  collect: ({ stats }) => ({
    toolCalls: stats.toolCallCount,
  }),
  reduce: (collected) => [{
    'Total Tool Calls': collected.reduce((s, c) => s + (c.values.toolCalls as number || 0), 0),
    'Sessions': collected.length,
  }],
}, {
  condition: ({ stats }) => stats.toolCallCount > 0,
});