Skip to content

TypeScript Agent

This walkthrough covers the TypeScript agent example -- three progressively complex LangChain workflows running on Node.js, all automatically instrumented with the Coalex TypeScript SDK.

Source: examples/agent-typescript/src/index.ts


What This Example Demonstrates

  • autoInstrument() capturing all LangChain spans with zero manual code
  • Three LangChain patterns: simple invocation, LCEL chain, multi-step reasoning
  • coalexContext() scoping each example as a separate trace
  • Multi-provider support: Vertex AI (Gemini), OpenAI, Anthropic
  • TypeScript-native usage of the @coalex-ai/sdk package

Prerequisites

Requirement Version
Node.js 18+
pnpm 9+
Coalex API key ck_live_...
LLM provider credentials See provider setup below

Setup

1. Start the local stack

docker compose up -d

2. Set environment variables

export COALEX_API_KEY="ck_live_..."
export COALEX_ENDPOINT="http://localhost:8080"

3. Choose your LLM provider

gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT="your-project"

Default model: gemini-2.0-flash

export COALEX_LLM_PROVIDER=openai
export OPENAI_API_KEY="sk-..."

Default model: gpt-4o

export COALEX_LLM_PROVIDER=anthropic
export ANTHROPIC_API_KEY="sk-ant-..."

Default model: claude-3-5-sonnet-20241022

Custom model override

You can override the default model for any provider:

export COALEX_MODEL="gemini-1.5-pro"

4. Install dependencies and run

cd examples/agent-typescript
pnpm install
pnpm exec tsx src/index.ts

Code Walkthrough

Coalex Setup

The setup mirrors the Python SDK pattern -- register once, auto-instrument once:

import { register, coalexContext, autoInstrument } from "@coalex-ai/sdk";

// 1. Register Coalex
register({
  endpoint: process.env.COALEX_ENDPOINT ?? "http://localhost:8080",
  apiKey,
  serviceName: "typescript-demo",
});

// 2. Auto-instrument LLM libraries
const results = autoInstrument();

After autoInstrument(), every LangChain LLM call, chain invocation, and prompt template renders as an OpenInference span -- no decorators or manual span creation needed.

The function returns a record of library names to status ("success" or an error), so you can confirm which instrumentors activated:

const instrumented = Object.entries(results)
  .filter(([, v]) => v === "success")
  .map(([k]) => k);
console.log(`Auto-instrumented: ${instrumented.join(", ")}`);

Provider Factory

The example includes a dynamic provider factory that lazy-imports the correct LangChain chat model based on the COALEX_LLM_PROVIDER environment variable:

import type { BaseChatModel } from "@langchain/core/language_models/chat_models";

const PROVIDER_DEFAULTS: Record<string, string> = {
  vertex: "gemini-2.0-flash",
  openai: "gpt-4o",
  anthropic: "claude-3-5-sonnet-20241022",
};

async function createLlm(provider: string, model?: string): Promise<BaseChatModel> {
  const resolvedModel = model || PROVIDER_DEFAULTS[provider] || "";

  if (provider === "openai") {
    const { ChatOpenAI } = await import("@langchain/openai");
    return new ChatOpenAI({ model: resolvedModel, temperature: 0.7 });
  }

  if (provider === "anthropic") {
    const { ChatAnthropic } = await import("@langchain/anthropic");
    return new ChatAnthropic({ model: resolvedModel, temperature: 0.7 });
  }

  // Default: vertex
  const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
  return new ChatGoogleGenerativeAI({
    model: resolvedModel,
    temperature: 0.7,
  });
}

Dynamic imports ensure only the selected provider's package is loaded at runtime.


Example 1: Simple LLM Invocation

The simplest possible instrumented call -- a direct LLM invocation wrapped in a Coalex context:

await coalexContext(
  { agentId: "demo-ts-simple", requestId: randomUUID(), version: "1.0.0" },
  async () => {
    const response = await llm.invoke("Explain what LangChain is in one sentence.");
    console.log(`Response: ${response.content}\n`);
  },
);

Key differences from the Python SDK:

  • coalexContext() takes a callback function (async closure) rather than using a with block
  • agentId and requestId use camelCase (TypeScript convention)
  • randomUUID() comes from Node.js node:crypto

Captured spans:

Trace: demo-ts-simple / req-<uuid>
  +-- ChatGoogleGenerativeAI (LLM)
       model: gemini-2.0-flash
       tokens_in: 12, tokens_out: ~35

Example 2: LCEL Chain (Prompt | LLM | Parser)

An LCEL chain composing a prompt template, LLM, and output parser -- using LangChain's .pipe() method (the TypeScript equivalent of Python's | operator):

import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";

await coalexContext(
  { agentId: "demo-ts-chain", requestId: randomUUID(), version: "1.0.0" },
  async () => {
    const prompt = ChatPromptTemplate.fromMessages([
      ["system", "You are a poet who writes in the style of {style}."],
      ["human", "Write a short poem about {topic}."],
    ]);
    const chain = prompt.pipe(llm).pipe(new StringOutputParser());
    const result = await chain.invoke({
      style: "Shakespeare",
      topic: "artificial intelligence",
    });
    console.log(`Poem:\n${result}\n`);
  },
);

Captured spans:

Trace: demo-ts-chain / req-<uuid>
  +-- RunnableSequence (CHAIN)
       +-- ChatPromptTemplate (CHAIN)
       +-- ChatGoogleGenerativeAI (LLM)
            model: gemini-2.0-flash
            input: "You are a poet... Write a short poem about artificial intelligence."
            output: "In silicon minds..."
       +-- StringOutputParser (CHAIN)

LCEL chain visibility

Auto-instrumentation captures each step of the LCEL pipeline as a child span. You can see the prompt template rendering, the LLM call with full input/output, and the parser step -- all without any manual instrumentation. The .pipe() calls produce the same span structure as Python's | operator.


Example 3: Multi-Step Reasoning Workflow

A three-step insurance claims processing workflow that chains multiple LLM calls:

await coalexContext(
  { agentId: "demo-ts-reasoning", requestId: randomUUID(), version: "1.0.0" },
  async () => {
    const claimText =
      "Patient John Doe, age 45, visited ER on Jan 15 2024 for chest pain. " +
      "ECG performed, troponin levels checked. Diagnosed with acute myocardial " +
      "infarction. Admitted for 3 days. Total billed: $15,000.";

    // Step 1: Extract key facts
    const facts = await llm.invoke(
      `Extract the key facts from this insurance claim as bullet points:\n\n${claimText}`,
    );
    console.log(`Step 1 - Extracted facts:\n${facts.content}\n`);

    // Step 2: Assess risk
    const assessment = await llm.invoke(
      `Based on these claim facts, assess the risk level (low/medium/high) ` +
        `and flag any anomalies:\n\n${facts.content}`,
    );
    console.log(`Step 2 - Risk assessment:\n${assessment.content}\n`);

    // Step 3: Generate recommendation (LCEL chain)
    const recPrompt = ChatPromptTemplate.fromMessages([
      ["system", "You are an insurance claims adjuster AI. Be concise."],
      [
        "human",
        "Claim facts:\n{facts}\n\nRisk assessment:\n{assessment}\n\n" +
          "Provide a one-paragraph recommendation: approve, deny, or flag for human review.",
      ],
    ]);
    const recChain = recPrompt.pipe(llm).pipe(new StringOutputParser());
    const recommendation = await recChain.invoke({
      facts: String(facts.content),
      assessment: String(assessment.content),
    });
    console.log(`Step 3 - Recommendation:\n${recommendation}\n`);
  },
);

Captured spans:

Trace: demo-ts-reasoning / req-<uuid>
  +-- ChatGoogleGenerativeAI (LLM) -- Step 1: Extract facts
  +-- ChatGoogleGenerativeAI (LLM) -- Step 2: Assess risk
  +-- RunnableSequence (CHAIN) -- Step 3: Recommendation
       +-- ChatPromptTemplate (CHAIN)
       +-- ChatGoogleGenerativeAI (LLM)
       +-- StringOutputParser (CHAIN)

All three steps are captured under the same trace, giving full visibility into the multi-step reasoning pipeline.


Provider Selection

The COALEX_LLM_PROVIDER environment variable controls which LangChain chat model is instantiated:

Value Chat Model Default Model Required Env Var
vertex (default) ChatGoogleGenerativeAI gemini-2.0-flash GOOGLE_CLOUD_PROJECT + ADC
openai ChatOpenAI gpt-4o OPENAI_API_KEY
anthropic ChatAnthropic claude-3-5-sonnet-20241022 ANTHROPIC_API_KEY

Override the model with COALEX_MODEL:

COALEX_LLM_PROVIDER=openai COALEX_MODEL=gpt-4o-mini pnpm exec tsx src/index.ts

What Gets Captured

For each example, Coalex auto-captures:

Data How
Model name (e.g., gemini-2.0-flash) Auto-instrumented LLM span attribute
Input messages Auto-instrumented input.value
Output messages Auto-instrumented output.value
Token counts (prompt + completion) Auto-instrumented token attributes
Latency per span OpenTelemetry span timing
LCEL chain structure Auto-instrumented chain spans
Agent ID and request ID coalexContext() propagation

Verifying Traces

After running the example, verify traces are captured:

Via the admin dashboard:

Open http://localhost:3000 and navigate to the Traces page. You should see three traces -- one per example (demo-ts-simple, demo-ts-chain, demo-ts-reasoning).

Via the API:

curl http://localhost:8080/v1/traces \
  -H "Authorization: Bearer $COALEX_API_KEY" | python -m json.tool

Via DuckLake directly:

duckdb -c "
  INSTALL ducklake; LOAD ducklake;
  ATTACH 'ducklake:postgresql://coalex:coalex@localhost:5432/ducklake' AS lake;
  SELECT trace_id, agent_id, span_count, duration_ms
  FROM lake.silver_traces
  ORDER BY ingested_at DESC
  LIMIT 5;
"

Python vs. TypeScript Comparison

If you are coming from the Python LangChain example, here is a quick comparison of the API differences:

Concept Python SDK TypeScript SDK
Import import coalex import { register, coalexContext, autoInstrument } from "@coalex-ai/sdk"
Register coalex.register(endpoint=..., service_name=..., api_key=...) register({ endpoint, serviceName, apiKey })
Auto-instrument coalex.auto_instrument() autoInstrument()
Context scope with coalex.coalex_context(agent_id=..., request_id=..., version=...): await coalexContext({ agentId, requestId, version }, async () => { ... })
LCEL pipe prompt \| llm \| parser prompt.pipe(llm).pipe(parser)
Package manager uv pnpm