Skip to content
Merged
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
72 changes: 36 additions & 36 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,43 +45,43 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT

# LangChain
langchain-latest-deps:
runs-on: ubuntu-latest
needs: get-changed-files
if: (contains(needs.get-changed-files.outputs.changed_files, 'dependency_range_tests/scripts/langchain/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-core/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/providers/langchain-openai/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-textsplitters/'))
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/[email protected]
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build required workspace packages
run: pnpm build --filter=@langchain/openai --filter=@langchain/anthropic --filter=@langchain/cohere --filter=@langchain/textsplitters
- name: Test LangChain with latest deps
run: docker compose -f dependency_range_tests/docker-compose.yml run langchain-latest-deps
# langchain-latest-deps:
# runs-on: ubuntu-latest
# needs: get-changed-files
# if: (contains(needs.get-changed-files.outputs.changed_files, 'dependency_range_tests/scripts/langchain/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-core/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/providers/langchain-openai/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-textsplitters/'))
# steps:
# - uses: actions/checkout@v4
# - name: Setup pnpm
# uses: pnpm/[email protected]
# - name: Use Node.js ${{ env.NODE_VERSION }}
# uses: actions/setup-node@v4
# with:
# node-version: ${{ env.NODE_VERSION }}
# - name: Install dependencies
# run: pnpm install --frozen-lockfile
# - name: Build required workspace packages
# run: pnpm build --filter=@langchain/openai --filter=@langchain/anthropic --filter=@langchain/cohere --filter=@langchain/textsplitters
# - name: Test LangChain with latest deps
# run: docker compose -f dependency_range_tests/docker-compose.yml run langchain-latest-deps

langchain-lowest-deps:
runs-on: ubuntu-latest
needs: get-changed-files
if: (contains(needs.get-changed-files.outputs.changed_files, 'dependency_range_tests/scripts/langchain/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain/'))
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/[email protected]
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build required workspace packages
run: pnpm build --filter=@langchain/openai --filter=@langchain/anthropic --filter=@langchain/cohere --filter=@langchain/textsplitters
- name: Test LangChain with lowest deps
run: docker compose -f dependency_range_tests/docker-compose.yml run langchain-lowest-deps
# langchain-lowest-deps:
# runs-on: ubuntu-latest
# needs: get-changed-files
# if: (contains(needs.get-changed-files.outputs.changed_files, 'dependency_range_tests/scripts/langchain/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain/'))
# steps:
# - uses: actions/checkout@v4
# - name: Setup pnpm
# uses: pnpm/[email protected]
# - name: Use Node.js ${{ env.NODE_VERSION }}
# uses: actions/setup-node@v4
# with:
# node-version: ${{ env.NODE_VERSION }}
# - name: Install dependencies
# run: pnpm install --frozen-lockfile
# - name: Build required workspace packages
# run: pnpm build --filter=@langchain/openai --filter=@langchain/anthropic --filter=@langchain/cohere --filter=@langchain/textsplitters
# - name: Test LangChain with lowest deps
# run: docker compose -f dependency_range_tests/docker-compose.yml run langchain-lowest-deps

# # Community
# community-latest-deps:
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"https://json.schemastore.org/github-workflow.json": "./.github/workflows/deploy.yml"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.experimental.useTsgo": true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Member Author

@christian-bromann christian-bromann Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will make your TS code 🚀

"cSpell.words": ["AILLM", "Upstash"],
"cSpell.enabledFileTypes": {
"mdx": true,
Expand Down
87 changes: 84 additions & 3 deletions libs/langchain-core/src/utils/types/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,29 @@ export type ZodObjectV3 = z3.ZodObject<any, any, any, any>;

export type ZodObjectV4 = z4.$ZodObject;

export type ZodDefaultV3<T extends z3.ZodTypeAny> = z3.ZodDefault<T>;
export type ZodDefaultV4<T extends z4.SomeType> = z4.$ZodDefault<T>;
export type ZodOptionalV3<T extends z3.ZodTypeAny> = z3.ZodOptional<T>;
export type ZodOptionalV4<T extends z4.SomeType> = z4.$ZodOptional<T>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InteropZodType<Output = any, Input = Output> =
| z3.ZodType<Output, z3.ZodTypeDef, Input>
| z4.$ZodType<Output, Input>;

export type InteropZodObject = ZodObjectV3 | ZodObjectV4;
export type InteropZodDefault<T = InteropZodObjectShape> =
T extends z3.ZodTypeAny
? ZodDefaultV3<T>
: T extends z4.SomeType
? ZodDefaultV4<T>
: never;
export type InteropZodOptional<T = InteropZodObjectShape> =
T extends z3.ZodTypeAny
? ZodOptionalV3<T>
: T extends z4.SomeType
? ZodOptionalV4<T>
: never;

export type InteropZodObjectShape<
T extends InteropZodObject = InteropZodObject
Expand Down Expand Up @@ -178,7 +195,7 @@ export async function interopSafeParseAsync<T>(
}
}
if (isZodSchemaV3(schema as z3.ZodType<Record<string, unknown>>)) {
return schema.safeParse(input);
return await schema.safeParseAsync(input);
}
throw new Error("Schema must be an instance of z3.ZodType or z4.$ZodType");
}
Expand All @@ -198,10 +215,10 @@ export async function interopParseAsync<T>(
input: unknown
): Promise<T> {
if (isZodSchemaV4(schema)) {
return parse(schema, input);
return await parseAsync(schema, input);
}
if (isZodSchemaV3(schema as z3.ZodType<Record<string, unknown>>)) {
return schema.parse(input);
return await schema.parseAsync(input);
}
throw new Error("Schema must be an instance of z3.ZodType or z4.$ZodType");
}
Expand Down Expand Up @@ -780,3 +797,67 @@ export function interopZodTransformInputSchema(

throw new Error("Schema must be an instance of z3.ZodType or z4.$ZodType");
}

/**
* Creates a modified version of a Zod object schema where fields matching a predicate are made optional.
* Supports both Zod v3 and v4 schemas and preserves the original schema version.
*
* @template T - The type of the Zod object schema.
* @param {T} schema - The Zod object schema instance (either v3 or v4).
* @param {(key: string, value: InteropZodType) => boolean} predicate - Function to determine which fields should be optional.
* @returns {InteropZodObject} The modified Zod object schema.
* @throws {Error} If the schema is not a Zod v3 or v4 object.
*/
export function interopZodObjectMakeFieldsOptional<T extends InteropZodObject>(
schema: T,
predicate: (key: string, value: InteropZodType) => boolean
): InteropZodObject {
if (isZodSchemaV3(schema)) {
const shape = getInteropZodObjectShape(schema);
const modifiedShape: Record<string, z3.ZodTypeAny> = {};

for (const [key, value] of Object.entries(shape)) {
if (predicate(key, value)) {
// Make this field optional using v3 methods
modifiedShape[key] = (value as z3.ZodTypeAny).optional();
} else {
// Keep field as-is
modifiedShape[key] = value;
}
}

// Use v3's extend method to create a new schema with the modified shape
return schema.extend(modifiedShape as z3.ZodRawShape);
}

if (isZodSchemaV4(schema)) {
const shape = getInteropZodObjectShape(schema);
const outputShape: Mutable<z4.$ZodShape> = { ...schema._zod.def.shape };

for (const [key, value] of Object.entries(shape)) {
if (predicate(key, value)) {
// Make this field optional using v4 methods
outputShape[key] = new $ZodOptional({
type: "optional" as const,
innerType: value as z4.$ZodType,
});
}
// Otherwise keep the field as-is (already in outputShape)
}

const modifiedSchema = clone<ZodObjectV4>(schema, {
...schema._zod.def,
shape: outputShape,
});

// Preserve metadata
const meta = globalRegistry.get(schema);
if (meta) globalRegistry.add(modifiedSchema, meta);

return modifiedSchema;
}

throw new Error(
"Schema must be an instance of z3.ZodObject or z4.$ZodObject"
);
}
2 changes: 1 addition & 1 deletion libs/langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"vitest": "^3.2.4"
},
"peerDependencies": {
"@langchain/core": "^1.0.0-alpha.3 <2.0.0",
"@langchain/core": "^1.0.0-alpha.5 <2.0.0",
"cheerio": "*",
"peggy": "^3.0.2",
"typeorm": "*"
Expand Down
2 changes: 1 addition & 1 deletion libs/langchain/src/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export {
} from "./responses.js";
export { createMiddleware } from "./middlewareAgent/index.js";
export type { AgentMiddleware } from "./middlewareAgent/types.js";

export type { ReactAgent } from "./middlewareAgent/ReactAgent.js";
/**
* Agents combine language models with tools to create systems that can reason
* about tasks, decide which tools to use, and iteratively work towards solutions.
Expand Down
32 changes: 23 additions & 9 deletions libs/langchain/src/agents/middlewareAgent/ReactAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ type MergedAgentState<
| ResponseFormatUndefined,
TMiddleware extends readonly AgentMiddleware<any, any, any>[]
> = (StructuredResponseFormat extends ResponseFormatUndefined
? BuiltInState
: BuiltInState & { structuredResponse: StructuredResponseFormat }) &
? Omit<BuiltInState, "jumpTo">
: Omit<BuiltInState, "jumpTo"> & {
structuredResponse: StructuredResponseFormat;
}) &
InferMiddlewareStates<TMiddleware>;

type InvokeStateParameter<
Expand Down Expand Up @@ -90,7 +92,11 @@ export class ReactAgent<
ContextSchema extends
| AnyAnnotationRoot
| InteropZodObject = AnyAnnotationRoot,
TMiddleware extends readonly AgentMiddleware<any, any, any>[] = []
TMiddleware extends readonly AgentMiddleware<
any,
any,
any
>[] = readonly AgentMiddleware<any, any, any>[]
> {
#graph: AgentGraph<StructuredResponseFormat, ContextSchema, TMiddleware>;

Expand All @@ -104,6 +110,14 @@ export class ReactAgent<
(Array.isArray(options.tools) ? options.tools : options.tools?.tools) ??
[];

/**
* append tools from middleware
*/
const middlewareTools = (this.options.middleware
?.filter((m) => m.tools)
.flatMap((m) => m.tools) ?? []) as (ClientTool | ServerTool)[];
toolClasses.push(...middlewareTools);

/**
* If any of the tools are configured to return_directly after running,
* our graph needs to check if these were called
Expand Down Expand Up @@ -662,9 +676,9 @@ export class ReactAgent<
/**
* Initialize middleware states if not already present in the input state.
*/
#initializeMiddlewareStates(
async #initializeMiddlewareStates(
state: InvokeStateParameter<TMiddleware>
): InvokeStateParameter<TMiddleware> {
): Promise<InvokeStateParameter<TMiddleware>> {
if (
!this.options.middleware ||
this.options.middleware.length === 0 ||
Expand All @@ -674,7 +688,7 @@ export class ReactAgent<
return state;
}

const defaultStates = initializeMiddlewareStates(
const defaultStates = await initializeMiddlewareStates(
this.options.middleware,
state
);
Expand Down Expand Up @@ -736,15 +750,15 @@ export class ReactAgent<
* console.log(result.structuredResponse.weather); // outputs: "It's sunny and 75°F."
* ```
*/
invoke(
async invoke(
state: InvokeStateParameter<TMiddleware>,
config?: InvokeConfiguration<
InferContextInput<ContextSchema> &
InferMiddlewareContextInputs<TMiddleware>
>
) {
type FullState = MergedAgentState<StructuredResponseFormat, TMiddleware>;
const initializedState = this.#initializeMiddlewareStates(state);
const initializedState = await this.#initializeMiddlewareStates(state);
return this.#graph.invoke(
initializedState,
config as unknown as InferContextInput<ContextSchema> &
Expand Down Expand Up @@ -808,7 +822,7 @@ export class ReactAgent<
InferMiddlewareContextInputs<TMiddleware>
>
): Promise<IterableReadableStream<any>> {
const initializedState = this.#initializeMiddlewareStates(state);
const initializedState = await this.#initializeMiddlewareStates(state);
return this.#graph.streamEvents(initializedState, {
...config,
version: "v2",
Expand Down
Loading
Loading