TypeScript TraceBuilder
Python equivalent: Python TraceBuilder
Use TraceBuilder when you already know what happened and need a trace without a provider adapter. TraceTree lets you inspect nested agent runs and sum their recorded metrics. Both work without making model calls.
Import
Section titled “Import”import { TraceBuilder, TraceTree } from '@attest-ai/core';TraceBuilder
Section titled “TraceBuilder”Fluent builder for constructing Trace objects. Setters and step-addition methods return this; build() returns a trace and getTraceId() returns its ID.
Constructor
Section titled “Constructor”class TraceBuilder { constructor(agentId?: string)}| Parameter | Type | Description |
|---|---|---|
agentId | string | undefined | Agent identifier. Stored as agent_id on the built trace. |
A random trace_id (format trc_<12-hex-chars>) is generated automatically.
setTraceId()
Section titled “setTraceId()”Override the auto-generated trace ID.
setTraceId(traceId: string): thisgetTraceId(): string returns the current ID. Changing it with setTraceId() does not rewrite parent links on previously built child traces.
setInput()
Section titled “setInput()”Set the trace input data.
setInput(input: Record<string, unknown>): thisbuilder.setInput({ question: "What is the capital of France?" });addLlmCall()
Section titled “addLlmCall()”Add an LLM call step to the trace.
addLlmCall( name: string, options?: { args?: Record<string, unknown>; result?: Record<string, unknown>; metadata?: Record<string, unknown>; },): thisbuilder.addLlmCall("gpt-4.1", { args: { prompt: "Translate to French", model: "gpt-4.1" }, result: { completion: "Bonjour le monde" }, metadata: { tokens: 42 },});addToolCall()
Section titled “addToolCall()”Add a tool call step to the trace.
addToolCall( name: string, options?: { args?: Record<string, unknown>; result?: Record<string, unknown>; metadata?: Record<string, unknown>; },): thisbuilder.addToolCall("web_search", { args: { query: "attest framework" }, result: { urls: ["https://example.com"] },});When an active mock registry contains the tool name, addToolCall() invokes the mock with the arguments object. A non-null, non-array object replaces the recorded result; a Promise is rejected because tool mocks must be synchronous. This does not execute the real tool or block unrelated network calls.
addRetrieval()
Section titled “addRetrieval()”Add a retrieval step (RAG context fetch) to the trace.
addRetrieval( name: string, options?: { args?: Record<string, unknown>; result?: Record<string, unknown>; metadata?: Record<string, unknown>; },): thisbuilder.addRetrieval("vector_db", { args: { query: "product specs", top_k: 5 }, result: { documents: ["doc1", "doc2"] },});addStep()
Section titled “addStep()”Add a complete Step object, such as an agent_call with sub_trace. Use this method for timing fields (started_at_ms, ended_at_ms), per-step agent_id / agent_role, and trace-v2 evidence; the convenience methods above do not accept those options.
addStep(step: Step): thisimport { STEP_AGENT_CALL } from '@attest-ai/core';
builder.addStep({ type: STEP_AGENT_CALL, name: "sub-agent", args: { task: "summarize" }, result: { summary: "..." }, sub_trace: childTrace,});setOutput()
Section titled “setOutput()”Set the trace output. Required before calling build().
setOutput(output: Record<string, unknown>): thisbuilder.setOutput({ message: "The capital of France is Paris." });setMetadata()
Section titled “setMetadata()”Set trace-level metadata (tokens, cost, latency, model, timestamp). This replaces the metadata object, unlike Python’s merging set_metadata().
setMetadata(metadata: { total_tokens?: number; cost_usd?: number; latency_ms?: number; model?: string; timestamp?: string;}): thisbuilder.setMetadata({ total_tokens: 150, cost_usd: 0.003, latency_ms: 450, model: "gpt-4.1",});setParentTraceId()
Section titled “setParentTraceId()”Link this trace to a parent trace (for delegation chains).
setParentTraceId(parentId: string): thisbuild()
Section titled “build()”Build the trace with schema_version: 1. Throws if setOutput() was not called. The step list is copied, but step objects, input/output and metadata remain shared references; this is not a deeply frozen snapshot.
build(): Traceconst trace = builder.build();// trace.trace_id, trace.agent_id, trace.input, trace.steps, trace.output, trace.metadataComplete Example
Section titled “Complete Example”const builder = new TraceBuilder("assistant");
const trace = builder .setInput({ question: "Summarize this article" }) .addRetrieval("vector_store", { args: { query: "article content", top_k: 3 }, result: { documents: ["chunk1", "chunk2", "chunk3"] }, }) .addLlmCall("gpt-4.1", { args: { prompt: "Summarize: chunk1, chunk2, chunk3" }, result: { completion: "This article discusses..." }, }) .setOutput({ message: "This article discusses..." }) .setMetadata({ total_tokens: 320, cost_usd: 0.006, latency_ms: 800 }) .build();Trace Interface
Section titled “Trace Interface”The interface is read-only at the type level, not deeply frozen at runtime. Builders emit trace schema 1; captured traces can use schema 2. This is independent of JSON-RPC protocol version 1.
interface Trace { readonly trace_id: string; readonly output: Record<string, unknown>; readonly schema_version?: number; readonly agent_id?: string; readonly input?: Record<string, unknown>; readonly steps: readonly Step[]; readonly metadata?: TraceMetadata; readonly parent_trace_id?: string; readonly conversation_id?: string; // Trace v2 readonly turns?: readonly Turn[]; readonly handoffs?: readonly Handoff[];}Step Interface
Section titled “Step Interface”interface Step { readonly type: string; // "llm_call" | "tool_call" | "retrieval" | "agent_call" readonly name: string; readonly args?: Record<string, unknown>; readonly result?: Record<string, unknown>; readonly sub_trace?: Trace; // Present for agent_call steps readonly metadata?: Record<string, unknown>; readonly started_at_ms?: number; readonly ended_at_ms?: number; readonly agent_id?: string; readonly agent_role?: string; readonly span_id?: string; // Trace v2 readonly parent_span_id?: string; readonly tool_call_id?: string; readonly retry_of?: number; readonly error?: StepError; readonly exchange?: Exchange;}Trace-v2 helper types describe the extra evidence: StepError has type and optional message; Exchange has request and response objects; Turn has an index, step_indices, and optional span_id, input, output; Handoff has from_agent, to_agent, and step_index. Build a schema-2 Trace directly (or preserve it from capture) when using these fields: the convenience builder still emits schema 1.
Step Type Constants
Section titled “Step Type Constants”import { STEP_LLM_CALL, // "llm_call" STEP_TOOL_CALL, // "tool_call" STEP_RETRIEVAL, // "retrieval" STEP_AGENT_CALL, // "agent_call"} from '@attest-ai/core';TraceMetadata Interface
Section titled “TraceMetadata Interface”interface TraceMetadata { readonly total_tokens?: number; readonly cost_usd?: number; readonly latency_ms?: number; readonly model?: string; readonly timestamp?: string;}TraceTree
Section titled “TraceTree”Read-only utility for traversing and aggregating multi-agent trace hierarchies. Operates on traces that contain agent_call steps with nested sub_trace objects.
Constructor
Section titled “Constructor”class TraceTree { constructor(root: Trace)}Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
root | Trace | The root trace (read-only). |
agents
Section titled “agents”Returns all agent IDs found in the tree (depth-first order).
get agents(): string[]const tree = new TraceTree(rootTrace);console.log(tree.agents); // ["orchestrator", "researcher", "writer"]Returns the maximum delegation depth. A trace with no sub-agents has depth 0.
get depth(): numberconsole.log(tree.depth); // 2 (orchestrator -> researcher -> sub-researcher)findAgent()
Section titled “findAgent()”Finds the trace for a specific agent ID. Returns undefined if not found.
findAgent(agentId: string): Trace | undefinedconst researcherTrace = tree.findAgent("researcher");if (researcherTrace) { console.log(researcherTrace.output);}flatten()
Section titled “flatten()”Returns all traces in the tree as a flat array (depth-first order).
flatten(): Trace[]const allTraces = tree.flatten();console.log(`Total agents: ${allTraces.length}`);delegations
Section titled “delegations”Returns all parent-child delegation pairs as [parentId, childId] tuples.
get delegations(): [string, string][]console.log(tree.delegations);// [["orchestrator", "researcher"], ["orchestrator", "writer"]]allToolCalls()
Section titled “allToolCalls()”Returns all tool call steps from every trace in the tree.
allToolCalls(): Step[]const tools = tree.allToolCalls();console.log(`Total tool calls: ${tools.length}`);Aggregate Metrics
Section titled “Aggregate Metrics”| Property | Type | Description |
|---|---|---|
aggregateTokens | number | Sum of metadata.total_tokens across all traces. |
aggregateCost | number | Sum of metadata.cost_usd across all traces. |
aggregateLatency | number | Sum of metadata.latency_ms across all traces. |
const tree = new TraceTree(rootTrace);console.log(`Total tokens: ${tree.aggregateTokens}`);console.log(`Total cost: $${tree.aggregateCost.toFixed(4)}`);console.log(`Total latency: ${tree.aggregateLatency}ms`);Complete Example
Section titled “Complete Example”import { TraceBuilder, TraceTree, STEP_AGENT_CALL } from '@attest-ai/core';
// Build root trace with delegationsconst researcher = new TraceBuilder("researcher") .setInput({ query: "AI testing" }) .addToolCall("search", { result: { count: 10 } }) .setOutput({ message: "Found 10 results" }) .setMetadata({ total_tokens: 200, cost_usd: 0.002 }) .build();
const writer = new TraceBuilder("writer") .setInput({ findings: "10 results" }) .addLlmCall("gpt-4.1", { result: { completion: "Summary..." } }) .setOutput({ message: "Summary..." }) .setMetadata({ total_tokens: 500, cost_usd: 0.01 }) .build();
const root = new TraceBuilder("orchestrator") .setInput({ topic: "AI testing" }) .addLlmCall("gpt-4.1", { result: { completion: "Plan: research then write" } }) .addStep({ type: STEP_AGENT_CALL, name: "researcher", sub_trace: researcher, result: researcher.output, }) .addStep({ type: STEP_AGENT_CALL, name: "writer", sub_trace: writer, result: writer.output, }) .setOutput({ message: "Research and writing complete" }) .setMetadata({ total_tokens: 100, cost_usd: 0.001 }) .build();
// Traverse the treeconst tree = new TraceTree(root);console.log(tree.agents); // ["orchestrator", "researcher", "writer"]console.log(tree.depth); // 1console.log(tree.delegations); // [["orchestrator", "researcher"], ["orchestrator", "writer"]]console.log(tree.aggregateTokens); // 800console.log(tree.aggregateCost); // 0.013console.log(tree.allToolCalls()); // [Step { type: "tool_call", name: "search", ... }]Tier Decorators
Section titled “Tier Decorators”Tier decorators tag test functions with assertion cost tiers for selective execution.
import { TIER_1, TIER_2, TIER_3, tier } from '@attest-ai/core';| Constant | Value | Assertion Layers | Cost |
|---|---|---|---|
TIER_1 | 1 | Schema, Constraint, Trace, Content (L1-L4) | Free |
TIER_2 | 2 | + Embedding (L5) | Local ONNX or hosted provider |
TIER_3 | 3 | + LLM Judge (L6) | Depends on judge provider |
tier()
Section titled “tier()”Returns a decorator that tags a function with a tier level.
function tier(level: number): <T extends (...args: never[]) => unknown>(fn: T) => Tconst testBasicOutput = tier(TIER_1)(() => { // Schema + constraint assertions only});
const testSemanticQuality = tier(TIER_3)(() => { // Includes LLM judge assertions});Use filterByTier() from @attest-ai/vitest to run tests up to a specific tier:
import { filterByTier } from '@attest-ai/vitest';
// Run only tier 1 tests (free, fast)const tier1Tests = filterByTier(allTests, TIER_1);Adapters
Section titled “Adapters”Adapters convert provider-specific response formats into Trace objects.
Release 1.1.0 has seven TypeScript adapters:
| Adapter | Maturity |
|---|---|
| OpenAI | Stable |
| Anthropic | Stable |
| Manual | Stable |
| Gemini | Beta |
| Ollama | Beta |
| LangChain.js | Beta |
| OpenTelemetry | Beta |
Framework and telemetry adapters use callbacks/spans rather than the single-response interface below. Provider tool-call steps represent model requests, not proof that a tool executed.
import { OpenAIAdapter, AnthropicAdapter, GeminiAdapter, OllamaAdapter, ManualAdapter, OTelAdapter, LangChainAdapter,} from '@attest-ai/core';Provider adapters expose traceFromResponse(), matching the TraceAdapter interface:
interface TraceAdapter { traceFromResponse(response: unknown, options?: Record<string, unknown>): Trace;}ManualAdapter exposes capture(builderFn) and createBuilder() instead.
OpenAIAdapter
Section titled “OpenAIAdapter”Converts OpenAI Chat Completions and Responses API results to traces, including refusals as output text.
class OpenAIAdapter { constructor(agentId?: string)
traceFromResponse( response: OpenAIChatCompletion | OpenAIResponse, options?: { inputMessages?: Record<string, unknown>[]; costUsd?: number; latencyMs?: number; startedAtMs?: number; endedAtMs?: number; structuredOutput?: Record<string, unknown>; }, ): Trace}const adapter = new OpenAIAdapter("assistant");
const response = await openai.chat.completions.create({ model: "gpt-4.1", messages: [{ role: "user", content: "Hello" }],});
const trace = adapter.traceFromResponse(response, { inputMessages: [{ role: "user", content: "Hello" }], costUsd: 0.002, latencyMs: 350,});
const chain = attestExpect(trace).outputContains("Hello");The adapter extracts:
- LLM call step from the completion
- Tool requests from Chat Completions
tool_callsor Responsesfunction_callitems - Total, input and output token counts when supplied by the provider
- Model name from
response.model
latencyMs takes precedence over endedAtMs - startedAtMs; no cost is inferred when costUsd is omitted. The default structured output is {} unless structuredOutput is supplied. See Framework Adapters for other integrations.