Skip to content

TypeScript Agent Class

Python equivalent: Python Expect DSL (the @agent decorator)

Agent records a function’s run as a trace so you can test it with assertion chains. Use run() for synchronous functions and arun() for asynchronous ones; use withTrace() when an adapter has already captured the trace.

import { Agent, agent, delegate, AgentResult } from '@attest-ai/core';

class Agent {
constructor(
name: string,
fn?: (builder: TraceBuilder, args: Record<string, unknown>) => unknown,
)
}
ParameterTypeDescription
namestringAgent identifier. Used as agent_id in traces.
fn(builder: TraceBuilder, args: Record<string, unknown>) => unknownAgent function that receives a TraceBuilder and input arguments. Return value becomes the trace output. Optional — omit when using withTrace().
PropertyTypeDescription
namestringThe agent identifier (read-only).

Executes the agent function synchronously. Creates a TraceBuilder, sets input, runs the function, captures output, and returns an AgentResult.

run(args?: Record<string, unknown>): AgentResult
const myAgent = new Agent("assistant", (builder, args) => {
builder.addLlmCall("gpt-4.1", {
args: { prompt: args.question },
result: { completion: "42" },
});
return { message: "42" };
});
const result = myAgent.run({ question: "What is the answer?" });

Output handling:

  • Non-null, non-array objects are used as-is for trace output.
  • Other values, including arrays and null, are wrapped as { result: <value> }.
  • undefined returns become { result: null }.

Async version of run(). Use when the agent function performs async operations.

async arun(args?: Record<string, unknown>): Promise<AgentResult>
const asyncAgent = new Agent("fetcher", async (builder, args) => {
const data = await fetchData(args.url);
builder.addToolCall("fetch", {
args: { url: args.url },
result: { data },
});
return { message: JSON.stringify(data) };
});
const result = await asyncAgent.arun({ url: "https://api.example.com/data" });

Creates an AgentResult from a pre-built Trace object, bypassing the agent function entirely. Useful when integrating adapter-captured traces.

withTrace(trace: Trace): AgentResult
import { OpenAIAdapter } from '@attest-ai/core';
const adapter = new OpenAIAdapter("assistant");
const trace = adapter.traceFromResponse(openaiResponse, {
inputMessages: messages,
costUsd: 0.003,
latencyMs: 450,
});
const myAgent = new Agent("assistant");
const result = myAgent.withTrace(trace);

A convenience function that creates an Agent and returns a callable wrapper. The returned function calls Agent.run() internally.

function agent(
name: string,
fn: (builder: TraceBuilder, args: Record<string, unknown>) => unknown,
): (args?: Record<string, unknown>) => AgentResult

The returned wrapper also exposes the underlying Agent instance via .agent.

const myAgent = agent("classifier", (builder, args) => {
builder.addLlmCall("gpt-4.1-mini", {
args: { text: args.text },
result: { completion: "positive" },
});
return { message: "positive", sentiment: "positive" };
});
// Call directly — no .run() needed
const result = myAgent({ text: "I love this product" });
// Access underlying Agent
const agentInstance = (myAgent as any).agent; // Agent instance

Delegates execution to a sub-agent within a parent agent’s run() context. Creates a child TraceBuilder, runs the sub-agent function, and records the delegation as an agent_call step on the parent trace.

async function delegate(
agentId: string,
fn: (child: TraceBuilder) => Promise<void> | void,
): Promise<void>
ParameterTypeDescription
agentIdstringIdentifier for the delegated sub-agent.
fn(child: TraceBuilder) => Promise<void> | voidFunction that builds the child trace. Receives a fresh TraceBuilder.

delegate() uses AsyncLocalStorage to find the parent TraceBuilder. It throws if called outside an Agent.run() or Agent.arun() context.

const orchestrator = new Agent("orchestrator", async (builder, args) => {
builder.addLlmCall("gpt-4.1", {
args: { task: "plan" },
result: { completion: "Delegating to researcher" },
});
await delegate("researcher", (child) => {
child.setInput({ query: args.query });
child.addToolCall("search", {
args: { q: args.query },
result: { results: ["result1", "result2"] },
});
child.setOutput({ message: "Found 2 results" });
});
return { message: "Research complete" };
});
const result = await orchestrator.arun({ query: "attest framework" });

The resulting trace contains an agent_call step with the child’s full trace embedded as sub_trace.

delegate() records the child trace and parent link, but does not populate step timestamps or agent identity on every child step. Timing assertions need that evidence explicitly (for example via addStep() with agent_id, started_at_ms, and ended_at_ms); delegation order alone is not timing evidence.


Holds the evaluation outcome: trace, assertion results, and aggregate metrics.

class AgentResult {
constructor(
trace: Trace,
assertionResults?: readonly AssertionResult[],
totalCost?: number,
totalDurationMs?: number,
usage?: Usage,
)
}
PropertyTypeDescription
traceTraceThe captured trace (read-only).
assertionResultsreadonly AssertionResult[]All assertion results after engine evaluation. Empty before evaluation.
totalCostnumberTotal evaluation cost in USD.
totalDurationMsnumberTotal evaluation duration in milliseconds.
usageUsageAttest model usage: model/judge calls, input/output/cached tokens, cost, retry cost, unpriced calls and budget refusals.
PropertyTypeDescription
passedbooleantrue if every assertion has status "pass".
failedAssertionsreadonly AssertionResult[]Assertions that did not pass (soft + hard failures).
hardFailuresreadonly AssertionResult[]Assertions with status "hard_fail".
softFailuresreadonly AssertionResult[]Assertions with status "soft_fail" (warnings).
passCountnumberCount of passed assertions.
failCountnumberCount of failed assertions (soft + hard).
notEvaluatedreadonly AssertionResult[]Assertions the engine did not evaluate; each carries a reason.
notEvaluatedReasonsReadonly<Record<string, number>>Count of not_evaluated assertions per reason.
passRatenumber | nullPassed / evaluated assertions; null when nothing was evaluated.
MethodReturnsDescription
outcome(options?: { strict?: boolean })"pass" | "fail" | "skip"Test outcome for the whole batch: any hard_fail fails; otherwise any not_evaluated skips (fails when strict); otherwise passes, including soft failures.
skipReason()string"not evaluated: <reason>[, ...]" text for skipped tests.

Constructor defaults are [], 0, 0 and zero usage for the optional arguments. Unknown result statuses are normalized to not_evaluated, not accepted as passes. passed is not the test verdict: use outcome() when soft failures and missing verdicts must be handled correctly.

const evaluated = await evaluate(chain);
console.log(`${evaluated.passCount}/${evaluated.assertionResults.length} passed`);
console.log(`Cost: $${evaluated.totalCost.toFixed(4)}`);
if (!evaluated.passed) {
for (const failure of evaluated.hardFailures) {
console.error(`FAIL: ${failure.explanation}`);
}
for (const warning of evaluated.softFailures) {
console.warn(`WARN: ${warning.explanation}`);
}
}

StatusConstantMeaning
"pass"STATUS_PASSAssertion succeeded.
"soft_fail"STATUS_SOFT_FAILNon-blocking failure, from an explicit soft assertion or a semantic score in its soft band. Reported as a warning.
"hard_fail"STATUS_HARD_FAILAssertion failed. Test should fail.
"not_evaluated"STATUS_NOT_EVALUATEDEngine did not evaluate the assertion (for example, embedding and judge layers in simulation mode). Test is skipped, or fails under strict mode.

import { Agent, attestExpect, delegate } from '@attest-ai/core';
import { evaluate } from '@attest-ai/vitest';
import { describe, it, expect } from 'vitest';
const pipeline = new Agent("orchestrator", async (builder, args) => {
builder.addLlmCall("gpt-4.1", {
args: { task: "coordinate" },
result: { completion: "Delegating research and writing" },
});
await delegate("researcher", (child) => {
child.setInput({ topic: args.topic });
child.addToolCall("web_search", {
args: { query: args.topic },
result: { articles: 5 },
});
child.setOutput({ message: "Found 5 relevant articles" });
});
await delegate("writer", (child) => {
child.setInput({ findings: "5 articles" });
child.addLlmCall("gpt-4.1", {
args: { task: "write summary" },
result: { completion: "Summary of findings..." },
});
child.setOutput({ message: "Summary of findings..." });
});
return { message: "Research and summary complete" };
});
describe("multi-agent pipeline", () => {
it("delegates correctly", async (context) => {
const result = await pipeline.arun({ topic: "AI testing" });
const chain = attestExpect(result)
.agentCalled("researcher")
.agentCalled("writer")
.delegationDepth(1);
const evaluated = await evaluate(chain, { context });
expect(evaluated.passed).toBe(true);
});
});