@temporalio/langsmith 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/lib/activity-interceptor.d.ts +21 -0
- package/lib/activity-interceptor.js +121 -0
- package/lib/activity-interceptor.js.map +1 -0
- package/lib/client-interceptor.d.ts +11 -0
- package/lib/client-interceptor.js +82 -0
- package/lib/client-interceptor.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -0
- package/lib/plugin.d.ts +88 -0
- package/lib/plugin.js +235 -0
- package/lib/plugin.js.map +1 -0
- package/lib/propagation.d.ts +57 -0
- package/lib/propagation.js +134 -0
- package/lib/propagation.js.map +1 -0
- package/lib/run-tree.d.ts +94 -0
- package/lib/run-tree.js +319 -0
- package/lib/run-tree.js.map +1 -0
- package/lib/sinks.d.ts +54 -0
- package/lib/sinks.js +72 -0
- package/lib/sinks.js.map +1 -0
- package/lib/workflow-interceptors.d.ts +14 -0
- package/lib/workflow-interceptors.js +350 -0
- package/lib/workflow-interceptors.js.map +1 -0
- package/package.json +88 -0
- package/src/activity-interceptor.ts +139 -0
- package/src/client-interceptor.ts +109 -0
- package/src/index.ts +8 -0
- package/src/plugin.ts +300 -0
- package/src/propagation.ts +153 -0
- package/src/run-tree.ts +330 -0
- package/src/sinks.ts +119 -0
- package/src/workflow-interceptors.ts +532 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-isolate interceptors and the deterministic LangSmith context
|
|
3
|
+
* provider. Loaded into the workflow bundle (via the plugin's `workflowModules`)
|
|
4
|
+
* and runs entirely inside the V8 isolate, where `node:async_hooks` is
|
|
5
|
+
* unavailable so a synchronous stack-based context provider stands in for
|
|
6
|
+
* LangSmith's async-context store.
|
|
7
|
+
*
|
|
8
|
+
* Internal: the worker loads this module by specifier and the bundler aliases
|
|
9
|
+
* `node:async_hooks` to it — it is not a hand-import API.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { RunTree } from 'langsmith/run_trees';
|
|
16
|
+
import { AsyncLocalStorageProviderSingleton } from 'langsmith/singletons/traceable';
|
|
17
|
+
import type {
|
|
18
|
+
ContinueAsNewInput,
|
|
19
|
+
Next,
|
|
20
|
+
WorkflowExecuteInput,
|
|
21
|
+
WorkflowInboundCallsInterceptor,
|
|
22
|
+
WorkflowInterceptors,
|
|
23
|
+
WorkflowOutboundCallsInterceptor,
|
|
24
|
+
} from '@temporalio/workflow';
|
|
25
|
+
import {
|
|
26
|
+
workflowInfo,
|
|
27
|
+
type ActivityInput,
|
|
28
|
+
type Headers,
|
|
29
|
+
type LocalActivityInput,
|
|
30
|
+
type QueryInput,
|
|
31
|
+
type SignalInput,
|
|
32
|
+
type SignalWorkflowInput,
|
|
33
|
+
type StartChildWorkflowExecutionInput,
|
|
34
|
+
type StartNexusOperationInput,
|
|
35
|
+
type StartNexusOperationOutput,
|
|
36
|
+
type UpdateInput,
|
|
37
|
+
} from '@temporalio/workflow';
|
|
38
|
+
import {
|
|
39
|
+
HEADER_KEY,
|
|
40
|
+
encodeContextString,
|
|
41
|
+
isInternalQuery,
|
|
42
|
+
readContextHeader,
|
|
43
|
+
scrubSensitive,
|
|
44
|
+
withContextHeader,
|
|
45
|
+
} from './propagation';
|
|
46
|
+
import {
|
|
47
|
+
ReplaySafeRunTree,
|
|
48
|
+
_RootReplaySafeRunTreeFactory,
|
|
49
|
+
RUN_TYPE,
|
|
50
|
+
asOutputs,
|
|
51
|
+
describeError,
|
|
52
|
+
emitMarkerRun,
|
|
53
|
+
handleQueryRunName,
|
|
54
|
+
handleSignalRunName,
|
|
55
|
+
handleUpdateRunName,
|
|
56
|
+
runHeaders,
|
|
57
|
+
runTreeFromContext,
|
|
58
|
+
runWorkflowRunName,
|
|
59
|
+
signalChildWorkflowRunName,
|
|
60
|
+
signalExternalWorkflowRunName,
|
|
61
|
+
startActivityRunName,
|
|
62
|
+
startChildWorkflowRunName,
|
|
63
|
+
startNexusOperationRunName,
|
|
64
|
+
validateUpdateRunName,
|
|
65
|
+
} from './run-tree';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Configuration the worker injects into the workflow bundle via the bundler's
|
|
69
|
+
* `DefinePlugin`. Mirrors the user-facing plugin options that the workflow
|
|
70
|
+
* isolate needs in order to decide whether to emit Temporal-operation runs.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export interface WorkflowLangSmithConfig {
|
|
75
|
+
addTemporalRuns: boolean;
|
|
76
|
+
projectName?: string;
|
|
77
|
+
tags?: string[];
|
|
78
|
+
metadata?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Injected at bundle time by the plugin's DefinePlugin. Declared so the bare
|
|
82
|
+
// identifier is replaced textually (avoids the dotted-access DefinePlugin
|
|
83
|
+
// pitfall). Absent in environments that bundled without the plugin.
|
|
84
|
+
declare const __TEMPORAL_LANGSMITH_CONFIG__: string | undefined;
|
|
85
|
+
|
|
86
|
+
function readConfig(): WorkflowLangSmithConfig {
|
|
87
|
+
try {
|
|
88
|
+
if (typeof __TEMPORAL_LANGSMITH_CONFIG__ === 'string') {
|
|
89
|
+
return JSON.parse(__TEMPORAL_LANGSMITH_CONFIG__) as WorkflowLangSmithConfig;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
/* fall through to default */
|
|
93
|
+
}
|
|
94
|
+
return { addTemporalRuns: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Synchronous, isolate-safe replacement for LangSmith's async-context store,
|
|
99
|
+
* shared by the installed provider, the {@link AsyncLocalStorage} shim, and
|
|
100
|
+
* every interceptor so they all read and write one store. `workflowAmbient`
|
|
101
|
+
* persists across `await` boundaries; `stack` tracks synchronous `traceable`
|
|
102
|
+
* nesting. Under `Promise.all` fan-out parenting falls back to the ambient —
|
|
103
|
+
* trace-shape-only non-determinism, never workflow history.
|
|
104
|
+
*/
|
|
105
|
+
class WorkflowContextManager {
|
|
106
|
+
private workflowAmbient: RunTree | undefined;
|
|
107
|
+
private readonly stack: (RunTree | undefined)[] = [];
|
|
108
|
+
|
|
109
|
+
getStore(): RunTree | undefined {
|
|
110
|
+
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : this.workflowAmbient;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Push `context`, run `fn`, pop in `finally`, and return `fn`'s result — the
|
|
115
|
+
* `AsyncLocalStorage.run` contract langsmith relies on.
|
|
116
|
+
*/
|
|
117
|
+
run<T>(context: RunTree | undefined, fn: () => T): T {
|
|
118
|
+
this.stack.push(context);
|
|
119
|
+
try {
|
|
120
|
+
return fn();
|
|
121
|
+
} finally {
|
|
122
|
+
this.stack.pop();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The ambient run an outbound interceptor should propagate / parent under. */
|
|
127
|
+
ambient(): RunTree | undefined {
|
|
128
|
+
return this.getStore();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Run an async scope with a dedicated ambient (workflow execute / handler). */
|
|
132
|
+
async withAmbient<T>(run: RunTree | undefined, fn: () => Promise<T>): Promise<T> {
|
|
133
|
+
const prev = this.workflowAmbient;
|
|
134
|
+
this.workflowAmbient = run;
|
|
135
|
+
try {
|
|
136
|
+
return await fn();
|
|
137
|
+
} finally {
|
|
138
|
+
this.workflowAmbient = prev;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let providerInstalled = false;
|
|
144
|
+
function ensureProviderInstalled(manager: WorkflowContextManager): void {
|
|
145
|
+
if (providerInstalled) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
providerInstalled = true;
|
|
149
|
+
// LangSmith's `AsyncLocalStorageInterface` requires exactly two members,
|
|
150
|
+
// `getStore` and `run` (it never calls `enterWith`), so we provide only those.
|
|
151
|
+
// The cast bridges the generic `run<T>` signature to the interface's
|
|
152
|
+
// `run: (ctx, () => void) => void`.
|
|
153
|
+
AsyncLocalStorageProviderSingleton.initializeGlobalInstance({
|
|
154
|
+
getStore: () => manager.getStore(),
|
|
155
|
+
run: <T>(context: RunTree | undefined, fn: () => T): T => manager.run(context as RunTree | undefined, fn),
|
|
156
|
+
} as Parameters<typeof AsyncLocalStorageProviderSingleton.initializeGlobalInstance>[0]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sharedManager = new WorkflowContextManager();
|
|
160
|
+
ensureProviderInstalled(sharedManager);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Isolate-safe stand-in for Node's `AsyncLocalStorage`. The plugin's bundler
|
|
164
|
+
* aliases `node:async_hooks` (absent in the isolate) to this module so
|
|
165
|
+
* LangSmith's `import { AsyncLocalStorage } from "node:async_hooks"` resolves
|
|
166
|
+
* here. Every instance delegates to {@link sharedManager}.
|
|
167
|
+
*
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
export class AsyncLocalStorage<T = unknown> {
|
|
171
|
+
getStore(): T | undefined {
|
|
172
|
+
return sharedManager.getStore() as unknown as T | undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
run<R>(store: T, fn: (...args: never[]) => R): R {
|
|
176
|
+
return sharedManager.run(store as unknown as RunTree | undefined, fn as () => R);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
enterWith(_store: T): void {
|
|
180
|
+
/* no-op: parenting inside the isolate is stack-scoped via run() */
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Best-effort equivalent of `AsyncLocalStorage.snapshot()` (used only by
|
|
185
|
+
* LangSmith's async-generator streaming paths). Runs the callback in the
|
|
186
|
+
* current synchronous scope; the isolate has no async context to capture.
|
|
187
|
+
*/
|
|
188
|
+
static snapshot(): (fn: (...args: unknown[]) => unknown, ...args: unknown[]) => unknown {
|
|
189
|
+
return (fn, ...args) => fn(...args);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Reconstruct the propagated parent run from a Payload-keyed header map. */
|
|
194
|
+
function reconstructParent(headers: Record<string, unknown> | undefined): RunTree | undefined {
|
|
195
|
+
return runTreeFromContext(readContextHeader(headers as Record<string, never> | undefined));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Re-wrap a reconstructed (plain) parent so its `createChild` produces
|
|
200
|
+
* replay-safe children. Carries the same id/trace/dotted so descendants parent
|
|
201
|
+
* under the real propagated run; never emitted itself.
|
|
202
|
+
*/
|
|
203
|
+
function asReplaySafeParent(
|
|
204
|
+
parent: RunTree | undefined,
|
|
205
|
+
generateNewUuid4?: () => string
|
|
206
|
+
): ReplaySafeRunTree | undefined {
|
|
207
|
+
if (!parent) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return new ReplaySafeRunTree(
|
|
211
|
+
{
|
|
212
|
+
name: parent.name,
|
|
213
|
+
run_type: parent.run_type,
|
|
214
|
+
id: parent.id,
|
|
215
|
+
trace_id: parent.trace_id,
|
|
216
|
+
dotted_order: parent.dotted_order,
|
|
217
|
+
parent_run_id: parent.parent_run_id,
|
|
218
|
+
project_name: parent.project_name,
|
|
219
|
+
},
|
|
220
|
+
generateNewUuid4
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A placeholder parent for the no-propagated-parent case. Installed as the ambient
|
|
226
|
+
* so a workflow-body `traceable` always nests via `createChild`; never emitted,
|
|
227
|
+
* and its children are independent roots (see {@link _RootReplaySafeRunTreeFactory}).
|
|
228
|
+
*/
|
|
229
|
+
function placeholderRoot(generateNewUuid4?: () => string): _RootReplaySafeRunTreeFactory {
|
|
230
|
+
return new _RootReplaySafeRunTreeFactory(
|
|
231
|
+
{ name: workflowInfo().workflowType, run_type: RUN_TYPE.CHAIN },
|
|
232
|
+
generateNewUuid4
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Emit a short-lived marker run as a child of `parent`. */
|
|
237
|
+
async function emitMarker(
|
|
238
|
+
parent: ReplaySafeRunTree,
|
|
239
|
+
name: string,
|
|
240
|
+
inputs: Record<string, unknown>
|
|
241
|
+
): Promise<ReplaySafeRunTree> {
|
|
242
|
+
const marker = parent.createChild({ name, run_type: RUN_TYPE.CHAIN, inputs });
|
|
243
|
+
await emitMarkerRun(marker);
|
|
244
|
+
return marker;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildReplaySafeRunTree(
|
|
248
|
+
config: WorkflowLangSmithConfig,
|
|
249
|
+
params: {
|
|
250
|
+
name: string;
|
|
251
|
+
runType: string;
|
|
252
|
+
parent: ReplaySafeRunTree | undefined;
|
|
253
|
+
inputs: Record<string, unknown>;
|
|
254
|
+
generateNewUuid4?: () => string;
|
|
255
|
+
}
|
|
256
|
+
): ReplaySafeRunTree {
|
|
257
|
+
return new ReplaySafeRunTree(
|
|
258
|
+
{
|
|
259
|
+
name: params.name,
|
|
260
|
+
run_type: params.runType,
|
|
261
|
+
parent_run: params.parent,
|
|
262
|
+
project_name: config.projectName,
|
|
263
|
+
tags: config.tags,
|
|
264
|
+
extra: { metadata: scrubSensitive(config.metadata) ?? {} },
|
|
265
|
+
inputs: params.inputs,
|
|
266
|
+
},
|
|
267
|
+
params.generateNewUuid4
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class LangSmithWorkflowInbound implements WorkflowInboundCallsInterceptor {
|
|
272
|
+
constructor(
|
|
273
|
+
private readonly ctx: WorkflowContextManager,
|
|
274
|
+
private readonly config: WorkflowLangSmithConfig
|
|
275
|
+
) {}
|
|
276
|
+
|
|
277
|
+
async execute(input: WorkflowExecuteInput, next: Next<WorkflowInboundCallsInterceptor, 'execute'>): Promise<unknown> {
|
|
278
|
+
const parent = reconstructParent(input.headers);
|
|
279
|
+
return this.runInbound(
|
|
280
|
+
runWorkflowRunName(workflowInfo().workflowType),
|
|
281
|
+
RUN_TYPE.CHAIN,
|
|
282
|
+
parent,
|
|
283
|
+
{ args: input.args },
|
|
284
|
+
() => next(input)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async handleSignal(input: SignalInput, next: Next<WorkflowInboundCallsInterceptor, 'handleSignal'>): Promise<void> {
|
|
289
|
+
const parent = reconstructParent(input.headers);
|
|
290
|
+
await this.runInbound(
|
|
291
|
+
handleSignalRunName(input.signalName),
|
|
292
|
+
RUN_TYPE.CHAIN,
|
|
293
|
+
parent,
|
|
294
|
+
{ args: input.args },
|
|
295
|
+
() => next(input),
|
|
296
|
+
true
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async handleQuery(input: QueryInput, next: Next<WorkflowInboundCallsInterceptor, 'handleQuery'>): Promise<unknown> {
|
|
301
|
+
if (isInternalQuery(input.queryName)) {
|
|
302
|
+
return next(input);
|
|
303
|
+
}
|
|
304
|
+
const parent = reconstructParent(input.headers);
|
|
305
|
+
return this.runInbound(
|
|
306
|
+
handleQueryRunName(input.queryName),
|
|
307
|
+
RUN_TYPE.CHAIN,
|
|
308
|
+
parent,
|
|
309
|
+
{ args: input.args },
|
|
310
|
+
() => next(input),
|
|
311
|
+
true,
|
|
312
|
+
// Read-only: mint run ids from the nondeterministic unsafe random source so the
|
|
313
|
+
// handler never draws from the Workflow main PRNG (which a clean replay would not advance).
|
|
314
|
+
workflowInfo().unsafe.random.uuid4
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async handleUpdate(
|
|
319
|
+
input: UpdateInput,
|
|
320
|
+
next: Next<WorkflowInboundCallsInterceptor, 'handleUpdate'>
|
|
321
|
+
): Promise<unknown> {
|
|
322
|
+
const parent = reconstructParent(input.headers);
|
|
323
|
+
return this.runInbound(
|
|
324
|
+
handleUpdateRunName(input.name),
|
|
325
|
+
RUN_TYPE.CHAIN,
|
|
326
|
+
parent,
|
|
327
|
+
{ args: input.args },
|
|
328
|
+
() => next(input),
|
|
329
|
+
true
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
validateUpdate(input: UpdateInput, next: Next<WorkflowInboundCallsInterceptor, 'validateUpdate'>): void {
|
|
334
|
+
// Read-only: mint run ids from the nondeterministic unsafe random source so the validator
|
|
335
|
+
// never draws from the Workflow main PRNG (which a clean replay would not advance).
|
|
336
|
+
const generateNewUuid4 = workflowInfo().unsafe.random.uuid4;
|
|
337
|
+
if (!this.config.addTemporalRuns) {
|
|
338
|
+
// Propagation only: install the reconstructed parent (or a placeholder parent
|
|
339
|
+
// when none was propagated, to keep a validator-body `traceable` off the
|
|
340
|
+
// no-parent `crypto` path) so the validator body nests under the update's
|
|
341
|
+
// trace. Validators are synchronous, so install via the stack-based `run`,
|
|
342
|
+
// not the async `withAmbient`.
|
|
343
|
+
const ambient =
|
|
344
|
+
asReplaySafeParent(reconstructParent(input.headers), generateNewUuid4) ?? placeholderRoot(generateNewUuid4);
|
|
345
|
+
this.ctx.run(ambient, () => next(input));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const run = buildReplaySafeRunTree(this.config, {
|
|
349
|
+
name: validateUpdateRunName(input.name),
|
|
350
|
+
runType: RUN_TYPE.CHAIN,
|
|
351
|
+
parent: asReplaySafeParent(reconstructParent(input.headers), generateNewUuid4),
|
|
352
|
+
inputs: { args: input.args },
|
|
353
|
+
generateNewUuid4,
|
|
354
|
+
});
|
|
355
|
+
// Validators are synchronous and must not be made async; emit start/end
|
|
356
|
+
// around the synchronous call, with the run installed on the stack so a
|
|
357
|
+
// `traceable` in the validator body nests under it.
|
|
358
|
+
void run.postRun();
|
|
359
|
+
try {
|
|
360
|
+
this.ctx.run(run, () => next(input));
|
|
361
|
+
void run.end({});
|
|
362
|
+
} catch (err) {
|
|
363
|
+
void run.end(undefined, describeError(err));
|
|
364
|
+
void run.patchRun();
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
void run.patchRun();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Open the inbound run, install it as the active run for `next`, and close it.
|
|
372
|
+
*
|
|
373
|
+
* `scoped` handlers install the run on the synchronous stack so a handler running
|
|
374
|
+
* concurrently with the workflow body cannot leak its run into the body's ambient;
|
|
375
|
+
* `execute` (scoped: false) installs a persistent ambient that survives the body's
|
|
376
|
+
* awaits.
|
|
377
|
+
*/
|
|
378
|
+
private async runInbound(
|
|
379
|
+
name: string,
|
|
380
|
+
runType: string,
|
|
381
|
+
parent: RunTree | undefined,
|
|
382
|
+
inputs: Record<string, unknown>,
|
|
383
|
+
next: () => Promise<unknown>,
|
|
384
|
+
scoped = false,
|
|
385
|
+
generateNewUuid4?: () => string
|
|
386
|
+
): Promise<unknown> {
|
|
387
|
+
if (!this.config.addTemporalRuns) {
|
|
388
|
+
// Propagation only: install the reconstructed parent as the active run so
|
|
389
|
+
// user `traceable` runs nest under it; never emit a Temporal-operation run.
|
|
390
|
+
// With no propagated parent, install a placeholder parent instead of
|
|
391
|
+
// `undefined` so a workflow-body `traceable` takes LangSmith's
|
|
392
|
+
// `createChild` branch (deterministic id) rather than the no-parent branch
|
|
393
|
+
// that mints a uuid via `crypto`, which the isolate lacks.
|
|
394
|
+
const ambient = asReplaySafeParent(parent, generateNewUuid4) ?? placeholderRoot(generateNewUuid4);
|
|
395
|
+
return scoped ? this.ctx.run(ambient, next) : this.ctx.withAmbient(ambient, next);
|
|
396
|
+
}
|
|
397
|
+
const run = buildReplaySafeRunTree(this.config, {
|
|
398
|
+
name,
|
|
399
|
+
runType,
|
|
400
|
+
parent: asReplaySafeParent(parent, generateNewUuid4),
|
|
401
|
+
inputs,
|
|
402
|
+
generateNewUuid4,
|
|
403
|
+
});
|
|
404
|
+
await run.postRun();
|
|
405
|
+
const body = async (): Promise<unknown> => {
|
|
406
|
+
try {
|
|
407
|
+
const result = await next();
|
|
408
|
+
await run.end(asOutputs(result));
|
|
409
|
+
await run.patchRun();
|
|
410
|
+
return result;
|
|
411
|
+
} catch (err) {
|
|
412
|
+
await run.end(undefined, describeError(err));
|
|
413
|
+
await run.patchRun();
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
return scoped ? this.ctx.run(run, body) : this.ctx.withAmbient(run, body);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
class LangSmithWorkflowOutbound implements WorkflowOutboundCallsInterceptor {
|
|
422
|
+
constructor(
|
|
423
|
+
private readonly ctx: WorkflowContextManager,
|
|
424
|
+
private readonly config: WorkflowLangSmithConfig
|
|
425
|
+
) {}
|
|
426
|
+
|
|
427
|
+
async scheduleActivity(
|
|
428
|
+
input: ActivityInput,
|
|
429
|
+
next: Next<WorkflowOutboundCallsInterceptor, 'scheduleActivity'>
|
|
430
|
+
): Promise<unknown> {
|
|
431
|
+
return this.peerMarker(startActivityRunName(input.activityType), input, (headers: Headers) =>
|
|
432
|
+
next({ ...input, headers })
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async scheduleLocalActivity(
|
|
437
|
+
input: LocalActivityInput,
|
|
438
|
+
next: Next<WorkflowOutboundCallsInterceptor, 'scheduleLocalActivity'>
|
|
439
|
+
): Promise<unknown> {
|
|
440
|
+
return this.peerMarker(startActivityRunName(input.activityType), input, (headers: Headers) =>
|
|
441
|
+
next({ ...input, headers })
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async startChildWorkflowExecution(
|
|
446
|
+
input: StartChildWorkflowExecutionInput,
|
|
447
|
+
next: Next<WorkflowOutboundCallsInterceptor, 'startChildWorkflowExecution'>
|
|
448
|
+
): Promise<[Promise<string>, Promise<unknown>]> {
|
|
449
|
+
return this.peerMarker(startChildWorkflowRunName(input.workflowType), input, (headers: Headers) =>
|
|
450
|
+
next({ ...input, headers })
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
continueAsNew(
|
|
455
|
+
input: ContinueAsNewInput,
|
|
456
|
+
next: Next<WorkflowOutboundCallsInterceptor, 'continueAsNew'>
|
|
457
|
+
): Promise<never> {
|
|
458
|
+
// Don't propagate a placeholder root: it is never emitted, so the successor's
|
|
459
|
+
// runs would dangle under a nonexistent parent. Let it install its own.
|
|
460
|
+
const ambient = this.ctx.ambient();
|
|
461
|
+
const context = ambient instanceof _RootReplaySafeRunTreeFactory ? undefined : runHeaders(ambient);
|
|
462
|
+
const headers = withContextHeader(input.headers, context);
|
|
463
|
+
return next({ ...input, headers });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async signalWorkflow(
|
|
467
|
+
input: SignalWorkflowInput,
|
|
468
|
+
next: Next<WorkflowOutboundCallsInterceptor, 'signalWorkflow'>
|
|
469
|
+
): Promise<void> {
|
|
470
|
+
const isChild = input.target.type === 'child';
|
|
471
|
+
const name = isChild
|
|
472
|
+
? signalChildWorkflowRunName(input.signalName)
|
|
473
|
+
: signalExternalWorkflowRunName(input.signalName);
|
|
474
|
+
return this.parentMarker(name, input, (headers: Headers) => next({ ...input, headers }));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async startNexusOperation(
|
|
478
|
+
input: StartNexusOperationInput,
|
|
479
|
+
next: Next<WorkflowOutboundCallsInterceptor, 'startNexusOperation'>
|
|
480
|
+
): Promise<StartNexusOperationOutput> {
|
|
481
|
+
const ambient = this.ctx.ambient();
|
|
482
|
+
if (this.config.addTemporalRuns && ambient instanceof ReplaySafeRunTree) {
|
|
483
|
+
await emitMarker(ambient, startNexusOperationRunName(input.service, input.operation), {});
|
|
484
|
+
}
|
|
485
|
+
const ctx = runHeaders(ambient);
|
|
486
|
+
const headers = ctx ? { ...input.headers, [HEADER_KEY]: encodeContextString(ctx) } : input.headers;
|
|
487
|
+
return next({ ...input, headers });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Sibling-marker operations: marker is a peer of the remote run; propagate ambient. */
|
|
491
|
+
private async peerMarker<R>(
|
|
492
|
+
name: string,
|
|
493
|
+
input: { headers: Headers; args?: unknown[] },
|
|
494
|
+
next: (headers: Headers) => Promise<R>
|
|
495
|
+
): Promise<R> {
|
|
496
|
+
const ambient = this.ctx.ambient();
|
|
497
|
+
if (this.config.addTemporalRuns && ambient instanceof ReplaySafeRunTree) {
|
|
498
|
+
await emitMarker(ambient, name, { args: input.args ?? [] });
|
|
499
|
+
}
|
|
500
|
+
const headers = withContextHeader(input.headers, runHeaders(ambient));
|
|
501
|
+
return next(headers);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Parent-marker operations: remote handler nests under the marker; propagate marker. */
|
|
505
|
+
private async parentMarker<R>(
|
|
506
|
+
name: string,
|
|
507
|
+
input: { headers: Headers; args?: unknown[] },
|
|
508
|
+
next: (headers: Headers) => Promise<R>
|
|
509
|
+
): Promise<R> {
|
|
510
|
+
const ambient = this.ctx.ambient();
|
|
511
|
+
let propagate: RunTree | undefined = ambient;
|
|
512
|
+
if (this.config.addTemporalRuns && ambient instanceof ReplaySafeRunTree) {
|
|
513
|
+
propagate = await emitMarker(ambient, name, { args: input.args ?? [] });
|
|
514
|
+
}
|
|
515
|
+
const headers = withContextHeader(input.headers, runHeaders(propagate));
|
|
516
|
+
return next(headers);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Workflow interceptors factory. Loaded by the Temporal worker for every
|
|
522
|
+
* workflow in the bundle (registered via the plugin's `workflowModules`).
|
|
523
|
+
*
|
|
524
|
+
* @internal
|
|
525
|
+
*/
|
|
526
|
+
export function interceptors(): WorkflowInterceptors {
|
|
527
|
+
const config = readConfig();
|
|
528
|
+
return {
|
|
529
|
+
inbound: [new LangSmithWorkflowInbound(sharedManager, config)],
|
|
530
|
+
outbound: [new LangSmithWorkflowOutbound(sharedManager, config)],
|
|
531
|
+
};
|
|
532
|
+
}
|