diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index a864ae6c3..00c6b3e33 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -26,6 +26,7 @@ import { import { StateBackend } from "./backends/index.js"; import { InteropZodObject } from "@langchain/core/utils/types"; import { CompiledSubAgent } from "./middleware/subagents.js"; +import { mergeMiddleware } from "./middleware/utils.js"; import type { CreateDeepAgentParams, DeepAgent, @@ -201,10 +202,10 @@ export function createDeepAgent< return { ...subagent, - middleware: [ - subagentSkillsMiddleware, - ...(subagent.middleware || []), - ] as readonly AgentMiddleware[], + middleware: mergeMiddleware( + [subagentSkillsMiddleware], + subagent.middleware || [], + ) as readonly AgentMiddleware[], }; }); @@ -293,13 +294,15 @@ export function createDeepAgent< * Runtime middleware array: combine built-in + optional middleware * Note: The type is handled separately via AllMiddleware type alias */ - const runtimeMiddleware: AgentMiddleware[] = [ - ...builtInMiddleware, - ...skillsMiddlewareArray, - ...memoryMiddlewareArray, - ...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []), - ...(customMiddleware as unknown as AgentMiddleware[]), - ]; + const runtimeMiddleware: AgentMiddleware[] = mergeMiddleware( + [ + ...builtInMiddleware, + ...skillsMiddlewareArray, + ...memoryMiddlewareArray, + ...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []), + ], + customMiddleware as unknown as AgentMiddleware[], + ); const agent = createAgent({ model, diff --git a/libs/deepagents/src/middleware/subagents.ts b/libs/deepagents/src/middleware/subagents.ts index 53383c04b..cda233baf 100644 --- a/libs/deepagents/src/middleware/subagents.ts +++ b/libs/deepagents/src/middleware/subagents.ts @@ -17,6 +17,7 @@ import { Command, getCurrentTaskInput } from "@langchain/langgraph"; import type { LanguageModelLike } from "@langchain/core/language_models/base"; import type { Runnable } from "@langchain/core/runnables"; import { HumanMessage } from "@langchain/core/messages"; +import { mergeMiddleware } from "./utils.js"; export type { AgentMiddleware }; @@ -476,7 +477,7 @@ function getSubagents(options: { agents[agentParams.name] = agentParams.runnable; } else { const middleware = agentParams.middleware - ? [...defaultSubagentMiddleware, ...agentParams.middleware] + ? mergeMiddleware(defaultSubagentMiddleware, agentParams.middleware) : [...defaultSubagentMiddleware]; const interruptOn = agentParams.interruptOn || defaultInterruptOn; diff --git a/libs/deepagents/src/middleware/utils.test.ts b/libs/deepagents/src/middleware/utils.test.ts index 0ad2c0eac..dcbf6fc24 100644 --- a/libs/deepagents/src/middleware/utils.test.ts +++ b/libs/deepagents/src/middleware/utils.test.ts @@ -1,6 +1,11 @@ import { describe, it, expect } from "vitest"; import { SystemMessage } from "@langchain/core/messages"; -import { appendToSystemMessage, prependToSystemMessage } from "./utils.js"; +import { + appendToSystemMessage, + prependToSystemMessage, + mergeMiddleware, +} from "./utils.js"; +import { AgentMiddleware, MIDDLEWARE_BRAND } from "langchain"; describe("appendToSystemMessage", () => { it("should create a new SystemMessage when original is null", () => { @@ -94,3 +99,111 @@ describe("prependToSystemMessage", () => { }); }); }); + +describe("mergeMiddleware", () => { + const createMockMiddleware = (name: string): AgentMiddleware => ({ + [MIDDLEWARE_BRAND]: true, + name, + }); + + it("should return defaults when custom is empty", () => { + const defaults = [createMockMiddleware("mw1"), createMockMiddleware("mw2")]; + const result = mergeMiddleware(defaults, []); + expect(result).toEqual(defaults); + expect(result).toHaveLength(2); + }); + + it("should return custom when defaults is empty", () => { + const custom = [createMockMiddleware("mw1"), createMockMiddleware("mw2")]; + const result = mergeMiddleware([], custom); + expect(result).toEqual(custom); + expect(result).toHaveLength(2); + }); + + it("should return empty when both are empty", () => { + expect(mergeMiddleware([], [])).toEqual([]); + }); + + it("should replace default with same-named custom in-place", () => { + const mw1 = createMockMiddleware("mw1"); + const mw2 = createMockMiddleware("mw2"); + const mw3 = createMockMiddleware("mw3"); + const mw2Override = createMockMiddleware("mw2"); + + const result = mergeMiddleware([mw1, mw2, mw3], [mw2Override]); + + expect(result).toHaveLength(3); + expect(result[0]).toBe(mw1); + expect(result[1]).toBe(mw2Override); // replaced in-place + expect(result[2]).toBe(mw3); + }); + + it("should append custom middleware that has no default counterpart", () => { + const mw1 = createMockMiddleware("mw1"); + const mw2 = createMockMiddleware("mw2"); + const mw4 = createMockMiddleware("mw4"); + + const result = mergeMiddleware([mw1, mw2], [mw4]); + + expect(result).toHaveLength(3); + expect(result[0]).toBe(mw1); + expect(result[1]).toBe(mw2); + expect(result[2]).toBe(mw4); + }); + + it("should handle mixed overrides and additions", () => { + const mw1 = createMockMiddleware("mw1"); + const mw2 = createMockMiddleware("mw2"); + const mw3 = createMockMiddleware("mw3"); + const mw2Override = createMockMiddleware("mw2"); + const mw4 = createMockMiddleware("mw4"); + + const result = mergeMiddleware([mw1, mw2, mw3], [mw2Override, mw4]); + + expect(result).toHaveLength(4); + expect(result[0]).toBe(mw1); + expect(result[1]).toBe(mw2Override); + expect(result[2]).toBe(mw3); + expect(result[3]).toBe(mw4); + }); + + it("should replace all defaults when all are overridden", () => { + const mw1 = createMockMiddleware("mw1"); + const mw2 = createMockMiddleware("mw2"); + const mw1Override = createMockMiddleware("mw1"); + const mw2Override = createMockMiddleware("mw2"); + + const result = mergeMiddleware([mw1, mw2], [mw1Override, mw2Override]); + + expect(result).toHaveLength(2); + expect(result[0]).toBe(mw1Override); + expect(result[1]).toBe(mw2Override); + }); + + it("should preserve default order for non-overridden middleware", () => { + const mw1 = createMockMiddleware("mw1"); + const mw2 = createMockMiddleware("mw2"); + const mw3 = createMockMiddleware("mw3"); + const mw1Override = createMockMiddleware("mw1"); + const mw3Override = createMockMiddleware("mw3"); + + const result = mergeMiddleware([mw1, mw2, mw3], [mw3Override, mw1Override]); + + expect(result).toHaveLength(3); + expect(result[0]).toBe(mw1Override); + expect(result[1]).toBe(mw2); + expect(result[2]).toBe(mw3Override); + }); + + it("should not mutate input arrays", () => { + const defaults = [createMockMiddleware("mw1"), createMockMiddleware("mw2")]; + const custom = [createMockMiddleware("mw2"), createMockMiddleware("mw3")]; + const defaultsCopy = [...defaults]; + const customCopy = [...custom]; + + mergeMiddleware(defaults, custom); + + expect(defaults).toEqual(defaultsCopy); + expect(custom).toEqual(customCopy); + }); +}); diff --git a/libs/deepagents/src/middleware/utils.ts b/libs/deepagents/src/middleware/utils.ts index 1612954d9..2ba53e16d 100644 --- a/libs/deepagents/src/middleware/utils.ts +++ b/libs/deepagents/src/middleware/utils.ts @@ -5,6 +5,7 @@ */ import { SystemMessage } from "@langchain/core/messages"; +import { AgentMiddleware } from "langchain"; /** * Append text to a system message. @@ -94,3 +95,45 @@ export function prependToSystemMessage( // Fallback for unknown content type return new SystemMessage({ content: text }); } + +/** + * Merge default and custom middleware arrays. + * + * Default middleware that share a name with a custom middleware are replaced + * in-place (preserving the default's position). Custom middleware that do not + * override any default are appended at the end. + * + * @param defaults - Base middleware array. + * @param custom - User-provided overrides and additions. + * @returns Merged middleware array with no duplicate names. + * + * @example + * ```typescript + * const defaults = [mw1, mw2, mw3]; + * const custom = [mw2Override, mw4]; + * const result = mergeMiddleware(defaults, custom); + * // Result: [mw1, mw2Override, mw3, mw4] + * ``` + */ +export function mergeMiddleware( + defaults: readonly AgentMiddleware[], + custom: readonly AgentMiddleware[], +): AgentMiddleware[] { + const customByName = new Map(); + for (const mw of custom) { + customByName.set(mw.name, mw); + } + + // Replace defaults in-place when a custom override exists + const merged = defaults.map((mw) => customByName.get(mw.name) ?? mw); + + // Append custom middleware that didn't override any default + const defaultNames = new Set(defaults.map((mw) => mw.name)); + for (const mw of custom) { + if (!defaultNames.has(mw.name)) { + merged.push(mw); + } + } + + return merged; +}