diff --git a/js/src/singletons/traceable.ts b/js/src/singletons/traceable.ts index 4d0744f2b..6319b99df 100644 --- a/js/src/singletons/traceable.ts +++ b/js/src/singletons/traceable.ts @@ -1,24 +1,42 @@ -import { RunTree, RunnableConfigLike } from "../run_trees.js"; +import { RunTree } from "../run_trees.js"; +import { TraceableFunction } from "./types.js"; -interface AsyncStorageLike { +interface AsyncLocalStorageInterface { getStore: () => RunTree | undefined; run: (context: RunTree | undefined, fn: () => void) => void; } -export const TraceableLocalStorageContext = (() => { - let storage: AsyncStorageLike; +class MockAsyncLocalStorage implements AsyncLocalStorageInterface { + getStore() { + return undefined; + } + + run(_: RunTree | undefined, callback: () => void): void { + return callback(); + } +} + +class AsyncLocalStorageProvider { + private asyncLocalStorage: AsyncLocalStorageInterface = + new MockAsyncLocalStorage(); - return { - register: (value: AsyncStorageLike) => { - storage ??= value; - return storage; - }, - get storage() { - return storage; - }, - }; -})(); + private hasBeenInitialized = false; + + getInstance(): AsyncLocalStorageInterface { + return this.asyncLocalStorage; + } + + initializeGlobalInstance(instance: AsyncLocalStorageInterface) { + if (!this.hasBeenInitialized) { + this.hasBeenInitialized = true; + this.asyncLocalStorage = instance; + } + } +} + +export const AsyncLocalStorageProviderSingleton = + new AsyncLocalStorageProvider(); /** * Return the current run tree from within a traceable-wrapped function. @@ -27,11 +45,7 @@ export const TraceableLocalStorageContext = (() => { * @returns The run tree for the given context. */ export const getCurrentRunTree = () => { - if (!TraceableLocalStorageContext.storage) { - throw new Error("Could not find the traceable storage context"); - } - - const runTree = TraceableLocalStorageContext.storage.getStore(); + const runTree = AsyncLocalStorageProviderSingleton.getInstance().getStore(); if (runTree === undefined) { throw new Error( [ @@ -47,79 +61,6 @@ export const getCurrentRunTree = () => { export const ROOT = Symbol.for("langsmith:traceable:root"); -type SmartPromise = T extends AsyncGenerator - ? T - : T extends Promise - ? T - : Promise; - -type WrapArgReturnPair = Pair extends [ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - infer Args extends any[], - infer Return -] - ? Args extends [RunTree, ...infer RestArgs] - ? { - ( - runTree: RunTree | typeof ROOT, - ...args: RestArgs - ): SmartPromise; - (config: RunnableConfigLike, ...args: RestArgs): SmartPromise; - } - : { - (...args: Args): SmartPromise; - (runTree: RunTree, ...rest: Args): SmartPromise; - (config: RunnableConfigLike, ...args: Args): SmartPromise; - } - : never; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type UnionToIntersection = (U extends any ? (x: U) => void : never) extends ( - x: infer I -) => void - ? I - : never; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type TraceableFunction any> = - // function overloads are represented as intersections rather than unions - // matches the behavior introduced in https://github.com/microsoft/TypeScript/pull/54448 - Func extends { - (...args: infer A1): infer R1; - (...args: infer A2): infer R2; - (...args: infer A3): infer R3; - (...args: infer A4): infer R4; - (...args: infer A5): infer R5; - } - ? UnionToIntersection< - WrapArgReturnPair<[A1, R1] | [A2, R2] | [A3, R3] | [A4, R4] | [A5, R5]> - > - : Func extends { - (...args: infer A1): infer R1; - (...args: infer A2): infer R2; - (...args: infer A3): infer R3; - (...args: infer A4): infer R4; - } - ? UnionToIntersection< - WrapArgReturnPair<[A1, R1] | [A2, R2] | [A3, R3] | [A4, R4]> - > - : Func extends { - (...args: infer A1): infer R1; - (...args: infer A2): infer R2; - (...args: infer A3): infer R3; - } - ? UnionToIntersection> - : Func extends { - (...args: infer A1): infer R1; - (...args: infer A2): infer R2; - } - ? UnionToIntersection> - : Func extends { - (...args: infer A1): infer R1; - } - ? UnionToIntersection> - : never; - export function isTraceableFunction( x: unknown // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/js/src/singletons/types.ts b/js/src/singletons/types.ts new file mode 100644 index 000000000..1ebe0eb19 --- /dev/null +++ b/js/src/singletons/types.ts @@ -0,0 +1,75 @@ +import { RunTree, RunnableConfigLike } from "../run_trees.js"; +import { ROOT } from "./traceable.js"; + +type SmartPromise = T extends AsyncGenerator + ? T + : T extends Promise + ? T + : Promise; +type WrapArgReturnPair = Pair extends [ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + infer Args extends any[], + infer Return +] + ? Args extends [RunTree, ...infer RestArgs] + ? { + ( + runTree: RunTree | typeof ROOT, + ...args: RestArgs + ): SmartPromise; + (config: RunnableConfigLike, ...args: RestArgs): SmartPromise; + } + : { + (...args: Args): SmartPromise; + (runTree: RunTree, ...rest: Args): SmartPromise; + (config: RunnableConfigLike, ...args: Args): SmartPromise; + } + : never; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends ( + x: infer I +) => void + ? I + : never; +// eslint-disable-next-line @typescript-eslint/no-explicit-any + +export type TraceableFunction any> = + // function overloads are represented as intersections rather than unions + // matches the behavior introduced in https://github.com/microsoft/TypeScript/pull/54448 + Func extends { + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; + } + ? UnionToIntersection< + WrapArgReturnPair<[A1, R1] | [A2, R2] | [A3, R3] | [A4, R4] | [A5, R5]> + > + : Func extends { + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + } + ? UnionToIntersection< + WrapArgReturnPair<[A1, R1] | [A2, R2] | [A3, R3] | [A4, R4]> + > + : Func extends { + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + } + ? UnionToIntersection> + : Func extends { + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + } + ? UnionToIntersection> + : Func extends { + (...args: infer A1): infer R1; + } + ? UnionToIntersection> + : never; + +export type RunTreeLike = RunTree; diff --git a/js/src/traceable.ts b/js/src/traceable.ts index ece357efe..82f86b616 100644 --- a/js/src/traceable.ts +++ b/js/src/traceable.ts @@ -11,9 +11,13 @@ import { InvocationParamsSchema, KVMap } from "./schemas.js"; import { isTracingEnabled } from "./env.js"; import { ROOT, - TraceableFunction, - TraceableLocalStorageContext, + AsyncLocalStorageProviderSingleton, } from "./singletons/traceable.js"; +import { TraceableFunction } from "./singletons/types.js"; + +AsyncLocalStorageProviderSingleton.initializeGlobalInstance( + new AsyncLocalStorage() +); function isPromiseMethod( x: string | symbol @@ -24,9 +28,20 @@ function isPromiseMethod( return false; } -const asyncLocalStorage = TraceableLocalStorageContext.register( - new AsyncLocalStorage() -); +function isKVMap(x: unknown): x is Record { + if (typeof x !== "object" || x == null) { + return false; + } + + const prototype = Object.getPrototypeOf(x); + return ( + (prototype === null || + prototype === Object.prototype || + Object.getPrototypeOf(prototype) === null) && + !(Symbol.toStringTag in x) && + !(Symbol.iterator in x) + ); +} const isAsyncIterable = (x: unknown): x is AsyncIterable => x != null && @@ -34,14 +49,14 @@ const isAsyncIterable = (x: unknown): x is AsyncIterable => // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof (x as any)[Symbol.asyncIterator] === "function"; -const GeneratorFunction = function* () {}.constructor; - const isIteratorLike = (x: unknown): x is Iterator => x != null && typeof x === "object" && "next" in x && typeof x.next === "function"; +const GeneratorFunction = function* () {}.constructor; + const isGenerator = (x: unknown): x is Generator => // eslint-disable-next-line no-instanceof/no-instanceof x != null && typeof x === "function" && x instanceof GeneratorFunction; @@ -379,6 +394,8 @@ export function traceable any>( }; } + const asyncLocalStorage = AsyncLocalStorageProviderSingleton.getInstance(); + // TODO: deal with possible nested promises and async iterables const processedArgs = args as unknown as Inputs; for (let i = 0; i < processedArgs.length; i++) { @@ -608,36 +625,9 @@ export function traceable any>( } export { - type TraceableFunction, getCurrentRunTree, isTraceableFunction, ROOT, } from "./singletons/traceable.js"; -export type RunTreeLike = RunTree; -function isKVMap(x: unknown): x is Record { - if (typeof x !== "object" || x == null) { - return false; - } - - const prototype = Object.getPrototypeOf(x); - return ( - (prototype === null || - prototype === Object.prototype || - Object.getPrototypeOf(prototype) === null) && - !(Symbol.toStringTag in x) && - !(Symbol.iterator in x) - ); -} - -export function wrapFunctionAndEnsureTraceable< - Func extends (...args: any[]) => any ->(target: Func, options: Partial, name = "target") { - if (typeof target === "function") { - return traceable(target, { - ...options, - name, - }); - } - throw new Error("Target must be runnable function"); -} +export type { RunTreeLike, TraceableFunction } from "./singletons/types.js";