Skip to content

Build on manni tracevals

Scope: consuming tracevals as a library rather than a CLI. It assumes the vocabulary from the journey pages, because a custom grader cannot be explained before the built-in graders are.

Terminal window
npm install -D @hawkeyexl/manni

ESM-only, Node.js 24 or newer, types shipped.

Every tool in the family is a namespace export of the one package, so tracevals is reached as tracevals:

import { tracevals } from "@hawkeyexl/manni";

That namespace exports a lot. These are the entry points meant for consumers; everything else is reachable but is there to serve the CLI.

You want to…Use
Run a full evaluationrunEvals(options) → RunReport
Read a trace without evaluatingparseTraceFile(path) → Trace
Find traces on diskdiscoverTraces(options) → TraceListing[]
Resolve which artifacts a trace usedresolveArtifacts(trace, options)
Add a check the built-ins do not coverregisterGrader(grader)
Import grader plugins the way the CLI doesloadGraderPlugins({ plugins, configDir })
Render a reportrender(report, format)
Load configurationloadConfig() / parseConfig(raw)
import { tracevals } from "@hawkeyexl/manni";
const { loadConfig, runEvals, render } = tracevals;
const config = await loadConfig();
const report = await runEvals({
tracePath: "./traces/session.jsonl",
projectDir: "./my-repo",
config,
deterministicOnly: true,
});
console.log(render(report, "markdown"));
console.log(report.summary, report.exitCode);

RunReport is the same structure --format json prints, documented field by field in Reports & exit codes. The full export list is in the API reference.

Every place tracevals reaches outside the process is injectable. Anything you build on top can keep its own suite hermetic, through the same seams this project’s tests use.

SeamWhereReplaces
EngineOptions.judgerunEvalsThe whole judged path. Pass your own TraceJudge, or set deterministicOnly and pass nothing.
EngineOptions.envrunEvalsEnvironment lookup, including CLAUDE_CONFIG_DIR. Point artifact resolution at a fixture tree.
ResolveOptions.envresolveArtifactsThe same, when resolving directly.
DiscoverOptions.envdiscoverTracesThe session store root.
promptFnpickTrace(options, promptFn)The interactive prompt, so the picker is testable without a TTY.
--localjudge constructionThe hosted provider. Inference runs on this machine with llama-cpp, so the judged path exercises end to end with no network call.
RunCommandOptions.require / FillOptions.requirerunRun, runFillGrader plugins, appended to config.plugins. Point it at a fixture module instead of an installed package.

A TraceJudge is just a function, so a stub is one line:

import { tracevals } from "@hawkeyexl/manni";
const { runEvals } = tracevals;
const alwaysPasses: tracevals.TraceJudge = async (plans) =>
plans.map((plan) => ({
evalName: plan.evalName,
artifact: plan.artifact.path,
artifactName: plan.artifact.name,
grader: plan.grader,
implicit: plan.implicit,
outcome: "pass",
durationMs: 0,
}));
const report = await runEvals({ tracePath, config, judge: alwaysPasses });

A stub judge is the fastest hermetic option. --local is the honest one, and the two answer different questions: a stub proves your wiring, and local inference proves the judged path.

parseTraceFile gives you the normalised model on its own, which is enough for analysis that has nothing to do with adherence:

import { tracevals } from "@hawkeyexl/manni";
const trace = await tracevals.parseTraceFile("./traces/session.jsonl");
console.log(trace.turnCount, trace.skillInvocations.map((s) => s.name));

Every field is described in Traces reference. Note usage is absent for Claude Code session files. Only stream-json transcripts carry it, which is why the cost grader skips rather than passes on the others.

TraceSource is a union with one member today, claude-code. Support for other formats is deferred rather than rejected, and the shape a new adapter has to conform to is the normalised Trace:

  • Required for anything to work: source, file, cwd, events, turnCount, and the derived collections (toolCalls, skillInvocations, agentSpawns, fileAccesses, userMessages, assistantTexts).
  • Degrades gracefully when absent: usage (the cost grader skips with a reason), model, gitBranch, sessionId, startedAt / endedAt.
  • Instruction semantics are the adapter’s job. What counts as a “skill” differs between agent tools; the adapter decides, and everything downstream consumes the normalised result.

Adding one is a change to this repository, not something you can register at runtime. The seam is a shape to conform to, not a plugin point.