Skip to content
Open
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
25 changes: 14 additions & 11 deletions libs/deepagents/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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,
Expand Down Expand Up @@ -200,10 +201,10 @@ export function createDeepAgent<

return {
...subagent,
middleware: [
subagentSkillsMiddleware,
...(subagent.middleware || []),
] as readonly AgentMiddleware[],
middleware: mergeMiddleware(
[subagentSkillsMiddleware],
subagent.middleware || [],
) as readonly AgentMiddleware[],
};
});

Expand Down Expand Up @@ -286,13 +287,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,
Expand Down
3 changes: 2 additions & 1 deletion libs/deepagents/src/middleware/subagents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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 };

Expand Down Expand Up @@ -451,7 +452,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;
Expand Down
115 changes: 114 additions & 1 deletion libs/deepagents/src/middleware/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
43 changes: 43 additions & 0 deletions libs/deepagents/src/middleware/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { SystemMessage } from "@langchain/core/messages";
import { AgentMiddleware } from "langchain";

/**
* Append text to a system message.
Expand Down Expand Up @@ -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<string, AgentMiddleware>();
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;
}
Loading