Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
early-return
empty
hello-world
langsmith
mutex
nestjs-exchange-rates
sleep-for-days
Expand Down
1 change: 1 addition & 0 deletions .scripts/list-of-samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"hello-world-mtls",
"interceptors-opentelemetry",
"lambda-worker",
"langsmith",
"monorepo-folders",
"mutex",
"nestjs-exchange-rates",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ and you'll be given the list of sample options.
- [**OpenTelemetry**](./interceptors-opentelemetry): Use the Interceptors feature to add OpenTelemetry metrics reporting to your workflows.
- [**Query Subscriptions**](./query-subscriptions): Use Redis Streams, Immer, and SDK Interceptors to subscribe to Workflow state.
- [**gRPC calls**](./grpc-calls): Make raw gRPC calls for advanced queries not covered by the WorkflowClient API.
- [**LangSmith**](./langsmith): Trace Workflows and Activities to [LangSmith](https://www.langchain.com/langsmith) using the `@temporalio/langsmith` plugin.

#### Test APIs

Expand Down
3 changes: 3 additions & 0 deletions langsmith/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
lib
.eslintrc.js
48 changes: 48 additions & 0 deletions langsmith/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { builtinModules } = require('module');

const ALLOWED_NODE_BUILTINS = new Set(['assert']);

module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'deprecation'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
rules: {
// recommended for safety
'@typescript-eslint/no-floating-promises': 'error', // forgetting to await Activities and Workflow APIs is bad
'deprecation/deprecation': 'warn',

// code style preference
'object-shorthand': ['error', 'always'],

// relaxed rules, for convenience
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'off',
},
overrides: [
{
files: ['src/workflows.ts', 'src/workflows-*.ts', 'src/workflows/*.ts'],
rules: {
'no-restricted-imports': [
'error',
...builtinModules.filter((m) => !ALLOWED_NODE_BUILTINS.has(m)).flatMap((m) => [m, `node:${m}`]),
],
},
},
],
};
2 changes: 2 additions & 0 deletions langsmith/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib
node_modules
1 change: 1 addition & 0 deletions langsmith/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
1 change: 1 addition & 0 deletions langsmith/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
20 changes: 20 additions & 0 deletions langsmith/.post-create
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
To begin development, install the Temporal CLI:

Mac: {cyan brew install temporal}
Other: Download and extract the latest release from https://github.com/temporalio/cli/releases/latest

Start Temporal Server:

{cyan temporal server start-dev}

Use Node version 18+ (v22.x is recommended):

Mac: {cyan brew install node@22}
Other: https://nodejs.org/en/download/

To see live traces in LangSmith, enable tracing:

{cyan export LANGSMITH_TRACING=true}
{cyan export LANGSMITH_API_KEY=...}

Then run a scenario by path (see each scenario's README).
1 change: 1 addition & 0 deletions langsmith/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
2 changes: 2 additions & 0 deletions langsmith/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
printWidth: 120
singleQuote: true
31 changes: 31 additions & 0 deletions langsmith/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# LangSmith Tracing for Temporal

These samples use the `@temporalio/langsmith` integration to add [LangSmith](https://docs.smith.langchain.com/) tracing to Temporal Workflows and Activities. Code you already instrument with LangSmith's native `traceable` keeps working unchanged when it runs inside a Workflow or Activity body — you only add the plugin to your Temporal Client and Worker, and the plugin threads a single trace across the `workflow → activity → child-workflow` boundaries.

This is a single project: one `package.json` and one set of configs at the `langsmith/` root, with each scenario in its own subdirectory. Run `npm install` once here, then run any scenario by path (see each scenario's README). The integration package itself is documented in the [`@temporalio/langsmith` README](https://github.com/temporalio/sdk-typescript/tree/main/contrib/langsmith).

## Prerequisites

These apply to every sample in this directory:

- A running Temporal dev server: `temporal server start-dev`.
- Node 22 or later.
- Dependencies installed once at the `langsmith/` root: `npm install`.

Tracing is **off by default**, matching the `langsmith` library. To see live traces in LangSmith, enable it in the Worker and Client process environment:

```bash
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=...
```

With tracing off the plugin is a no-op. The tests enable it in-process and assert against an in-memory LangSmith Client, so they need no API key.

## Samples

| Sample | Demonstrates |
| :-------------------------------------- | :------------------------------------------------------------------------------------ |
| [`activity-tracing`](./activity-tracing) | A `traceable` model call inside an Activity, nested under the Workflow and Activity runs. |
| [`workflow-tracing`](./workflow-tracing) | Replay-safe `traceable` calls in a Workflow body — emitted once, never duplicated on replay. |
| [`agent-pipeline`](./agent-pipeline) | A multi-step agent whose trace threads through Activities and a child Workflow. |
| [`message-handlers`](./message-handlers) | `traceable` calls inside Signal and Update handlers, nested under each handler's run. |
34 changes: 34 additions & 0 deletions langsmith/activity-tracing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Activity Tracing

A `traceable` model call runs inside an Activity body. The Client starts the Workflow from inside its own `traceable` (`user_pipeline`), so the whole call nests under one trace. With `addTemporalRuns: true` the plugin also emits the Temporal scaffolding runs (`StartWorkflow:`, `RunActivity:`, …).

## Run

Run these from the `langsmith/` root (run `npm install` there once first). To see live traces, `export LANGSMITH_TRACING=true` and `export LANGSMITH_API_KEY=...` in each terminal.

```bash
# In one terminal, start the Worker (requires a local Temporal server):
npx ts-node activity-tracing/src/worker.ts

# In another terminal, run the scenario:
npx ts-node activity-tracing/src/client.ts
```

## Test

```bash
npx mocha --exit --require ts-node/register --require source-map-support/register "activity-tracing/src/mocha/*.test.ts"
```

The test runs a real Worker against `TestWorkflowEnvironment` with an in-memory LangSmith Client, so no API key is required.

## Expected trace

```
user_pipeline
StartWorkflow:GreetingWorkflow
RunWorkflow:GreetingWorkflow
StartActivity:answer
RunActivity:answer
inner_llm_call
```
12 changes: 12 additions & 0 deletions langsmith/activity-tracing/src/activities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { traceable } from 'langsmith/traceable';

const callModel = traceable(
async (prompt: string): Promise<string> => {
return `answer to: ${prompt}`;
},
{ name: 'inner_llm_call' }
);

export async function answer(prompt: string): Promise<string> {
return callModel(prompt);
}
32 changes: 32 additions & 0 deletions langsmith/activity-tracing/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Connection, Client } from '@temporalio/client';
import { Client as LangSmithClient } from 'langsmith';
import { LangSmithPlugin } from '@temporalio/langsmith';
import { traceable } from 'langsmith/traceable';
import { nanoid } from 'nanoid';
import { GreetingWorkflow } from './workflows';

async function run() {
const connection = await Connection.connect();
const langsmith = new LangSmithClient();
const plugin = new LangSmithPlugin({ client: langsmith, addTemporalRuns: true });
const client = new Client({ connection, plugins: [plugin] });

const pipeline = traceable(
async () => {
return client.workflow.execute(GreetingWorkflow, {
taskQueue: 'langsmith-activity-tracing',
workflowId: 'langsmith-activity-tracing-' + nanoid(),
args: ['hello'],
});
},
{ name: 'user_pipeline' }
);

const result = await pipeline();
console.log(result);
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
110 changes: 110 additions & 0 deletions langsmith/activity-tracing/src/mocha/workflows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Tracing is off by default; enable it before the plugin is constructed.
process.env.LANGSMITH_TRACING = 'true';

import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import { Client } from '@temporalio/client';
import { Client as LangSmithClient } from 'langsmith';
import { LangSmithPlugin } from '@temporalio/langsmith';
import { after, before, describe, it } from 'mocha';
import assert from 'assert';
import * as activities from '../activities';
import { GreetingWorkflow } from '../workflows';

interface CollectedRun {
id: string;
name: string;
parent_run_id?: string;
}

class InMemoryRunCollector {
readonly createOrder: string[] = [];
readonly byId = new Map<string, CollectedRun>();

createRun = async (run: Record<string, unknown>): Promise<void> => {
const id = String(run.id);
if (!this.byId.has(id)) {
this.createOrder.push(id);
this.byId.set(id, { id, name: String(run.name) });
}
this.byId.set(id, { ...this.byId.get(id)!, ...(run as Partial<CollectedRun>), id });
};

updateRun = async (id: string, update: Record<string, unknown>): Promise<void> => {
const existing = this.byId.get(id);
if (existing) {
this.byId.set(id, { ...existing, ...(update as Partial<CollectedRun>), id });
}
};

awaitPendingTraceBatches = async (): Promise<void> => {};

byName(name: string): CollectedRun | undefined {
for (const id of this.createOrder) {
const run = this.byId.get(id)!;
if (run.name === name) {
return run;
}
}
return undefined;
}

parentNameOf(name: string): string | undefined {
const run = this.byName(name);
if (!run?.parent_run_id) {
return undefined;
}
return this.byId.get(run.parent_run_id)?.name;
}

asClient(): LangSmithClient {
return this as unknown as LangSmithClient;
}
}

describe('langsmith/activity-tracing', function () {
this.timeout(30_000);

let testEnv: TestWorkflowEnvironment;

before(async () => {
testEnv = await TestWorkflowEnvironment.createLocal();
});

after(async () => {
await testEnv?.teardown();
});

it('traces inner_llm_call under the activity run, nested below the workflow', async () => {
const collector = new InMemoryRunCollector();
const plugin = new LangSmithPlugin({ client: collector.asClient(), addTemporalRuns: true });
const taskQueue = 'test-langsmith-activity-tracing';

const worker = await Worker.create({
connection: testEnv.nativeConnection,
taskQueue,
workflowsPath: require.resolve('../workflows'),
activities,
plugins: [plugin],
});

const client = new Client({
connection: testEnv.connection,
namespace: testEnv.namespace,
plugins: [plugin],
});

const result = await worker.runUntil(
client.workflow.execute(GreetingWorkflow, {
args: ['hello'],
workflowId: 'test-langsmith-activity-tracing-' + Date.now(),
taskQueue,
})
);

assert.strictEqual(result, 'answer to: hello');

assert.strictEqual(collector.parentNameOf('inner_llm_call'), 'RunActivity:answer');
assert.strictEqual(collector.parentNameOf('RunActivity:answer'), 'RunWorkflow:GreetingWorkflow');
});
});
28 changes: 28 additions & 0 deletions langsmith/activity-tracing/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NativeConnection, Worker } from '@temporalio/worker';
import { Client as LangSmithClient } from 'langsmith';
import { LangSmithPlugin } from '@temporalio/langsmith';
import * as activities from './activities';

async function run() {
const connection = await NativeConnection.connect({ address: 'localhost:7233' });
try {
const langsmith = new LangSmithClient();
const plugin = new LangSmithPlugin({ client: langsmith, addTemporalRuns: true });

const worker = await Worker.create({
connection,
taskQueue: 'langsmith-activity-tracing',
workflowsPath: require.resolve('./workflows'),
activities,
plugins: [plugin],
});
await worker.run();
} finally {
await connection.close();
}
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
10 changes: 10 additions & 0 deletions langsmith/activity-tracing/src/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';

const { answer } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
});

export async function GreetingWorkflow(prompt: string): Promise<string> {
return answer(prompt);
}
Loading
Loading