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/sdkpackage
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¶
2. Set environment variables¶
3. Choose your LLM provider¶
Default model: gemini-2.0-flash
Custom model override
You can override the default model for any provider:
4. Install dependencies and run¶
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 awithblockagentIdandrequestIduse camelCase (TypeScript convention)randomUUID()comes from Node.jsnode: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:
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 |