TypeScript Agent Class
Python equivalent: Python Expect DSL (the
@agentdecorator)
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
Section titled “Import”import { Agent, agent, delegate, AgentResult } from '@attest-ai/core';Agent Class
Section titled “Agent Class”Constructor
Section titled “Constructor”class Agent { constructor( name: string, fn?: (builder: TraceBuilder, args: Record<string, unknown>) => unknown, )}| Parameter | Type | Description |
|---|---|---|
name | string | Agent identifier. Used as agent_id in traces. |
fn | (builder: TraceBuilder, args: Record<string, unknown>) => unknown | Agent function that receives a TraceBuilder and input arguments. Return value becomes the trace output. Optional — omit when using withTrace(). |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
name | string | The 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>): AgentResultconst 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> }. undefinedreturns become{ result: null }.
arun()
Section titled “arun()”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" });withTrace()
Section titled “withTrace()”Creates an AgentResult from a pre-built Trace object, bypassing the agent function entirely. Useful when integrating adapter-captured traces.
withTrace(trace: Trace): AgentResultimport { 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);agent() Factory
Section titled “agent() Factory”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>) => AgentResultThe 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() neededconst result = myAgent({ text: "I love this product" });
// Access underlying Agentconst agentInstance = (myAgent as any).agent; // Agent instancedelegate()
Section titled “delegate()”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>| Parameter | Type | Description |
|---|---|---|
agentId | string | Identifier for the delegated sub-agent. |
fn | (child: TraceBuilder) => Promise<void> | void | Function 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.
AgentResult
Section titled “AgentResult”Holds the evaluation outcome: trace, assertion results, and aggregate metrics.
class AgentResult { constructor( trace: Trace, assertionResults?: readonly AssertionResult[], totalCost?: number, totalDurationMs?: number, usage?: Usage, )}Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
trace | Trace | The captured trace (read-only). |
assertionResults | readonly AssertionResult[] | All assertion results after engine evaluation. Empty before evaluation. |
totalCost | number | Total evaluation cost in USD. |
totalDurationMs | number | Total evaluation duration in milliseconds. |
usage | Usage | Attest model usage: model/judge calls, input/output/cached tokens, cost, retry cost, unpriced calls and budget refusals. |
Computed Properties
Section titled “Computed Properties”| Property | Type | Description |
|---|---|---|
passed | boolean | true if every assertion has status "pass". |
failedAssertions | readonly AssertionResult[] | Assertions that did not pass (soft + hard failures). |
hardFailures | readonly AssertionResult[] | Assertions with status "hard_fail". |
softFailures | readonly AssertionResult[] | Assertions with status "soft_fail" (warnings). |
passCount | number | Count of passed assertions. |
failCount | number | Count of failed assertions (soft + hard). |
notEvaluated | readonly AssertionResult[] | Assertions the engine did not evaluate; each carries a reason. |
notEvaluatedReasons | Readonly<Record<string, number>> | Count of not_evaluated assertions per reason. |
passRate | number | null | Passed / evaluated assertions; null when nothing was evaluated. |
Methods
Section titled “Methods”| Method | Returns | Description |
|---|---|---|
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}`); }}Assertion Statuses
Section titled “Assertion Statuses”| Status | Constant | Meaning |
|---|---|---|
"pass" | STATUS_PASS | Assertion succeeded. |
"soft_fail" | STATUS_SOFT_FAIL | Non-blocking failure, from an explicit soft assertion or a semantic score in its soft band. Reported as a warning. |
"hard_fail" | STATUS_HARD_FAIL | Assertion failed. Test should fail. |
"not_evaluated" | STATUS_NOT_EVALUATED | Engine did not evaluate the assertion (for example, embedding and judge layers in simulation mode). Test is skipped, or fails under strict mode. |
Full Multi-Agent Example
Section titled “Full Multi-Agent Example”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); });});