From be94abd064975e62cc55e5d4ad47ef9fe22e0c1b Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Mon, 13 Jul 2026 12:52:37 -0700 Subject: [PATCH 1/9] fix(deepagents): merge custom middleware by name --- .changeset/soft-zebras-merge.md | 5 + libs/deepagents/src/agent.ts | 100 ++++---- .../src/middleware/subagent.test.ts | 232 +++++++++++++++++- .../src/middleware/tool_exclusion.ts | 32 +++ 4 files changed, 327 insertions(+), 42 deletions(-) create mode 100644 .changeset/soft-zebras-merge.md create mode 100644 libs/deepagents/src/middleware/tool_exclusion.ts diff --git a/.changeset/soft-zebras-merge.md b/.changeset/soft-zebras-merge.md new file mode 100644 index 000000000..6c83bb273 --- /dev/null +++ b/.changeset/soft-zebras-merge.md @@ -0,0 +1,5 @@ +--- +"deepagents": patch +--- + +fix(deepagents): allow custom middleware to replace defaults by name diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index 42b73b8f4..533dc782f 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -1,13 +1,11 @@ import { createAgent, - createMiddleware, humanInTheLoopMiddleware, anthropicPromptCachingMiddleware, bedrockPromptCachingMiddleware, todoListMiddleware, SystemMessage, type AgentMiddleware, - type AnyAgentMiddleware, context, } from "langchain"; import type { @@ -33,6 +31,7 @@ import { StateBackend } from "./backends/state.js"; import { ConfigurationError } from "./errors.js"; import { InteropZodObject } from "@langchain/core/utils/types"; import { createCacheBreakpointMiddleware } from "./middleware/cache.js"; +import { createToolExclusionMiddleware } from "./middleware/tool_exclusion.js"; import { GENERAL_PURPOSE_SUBAGENT, type CompiledSubAgent, @@ -108,6 +107,27 @@ const BUILTIN_TOOL_NAMES: ReadonlySet = new Set([ "write_todos", ]); +/** + * Merge custom middleware into an assembled stack by `.name`. + * + * Matching custom middleware replaces the existing entry in place. New + * middleware is appended after the base stack in caller-provided order. + * + * @internal + */ +export function mergeMiddleware( + base: readonly AgentMiddleware[], + custom: readonly AgentMiddleware[], +): AgentMiddleware[] { + const merged = new Map( + base.map((middleware) => [middleware.name, middleware]), + ); + for (const middleware of custom) { + merged.set(middleware.name, middleware); + } + return [...merged.values()]; +} + /** * Create a Deep Agent. * @@ -223,7 +243,7 @@ export function createDeepAgent< const anthropicModel = isAnthropicModel(model); const bedrockModel = isBedrockConverseModel(model); - let cacheMiddleware: AnyAgentMiddleware[] = []; + let cacheMiddleware: AgentMiddleware[] = []; if (anthropicModel) { cacheMiddleware = [ @@ -256,7 +276,7 @@ export function createDeepAgent< // Middleware for custom subagents (does NOT include skills from main agent). // Uses createSummarizationMiddleware (deepagents version) with backend support // and auto-computed defaults from model profile. - const subagentMiddleware = [ + const subagentCoreMiddleware = [ // Provides todo list management capabilities for tracking tasks. todoListMiddleware(), // Enables filesystem operations and optional long-term memory storage. @@ -274,11 +294,30 @@ export function createDeepAgent< ...(input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({ backend, sources: input.skills })] : []), - // Appends custom middleware from the subagent spec. - ...(input.middleware ?? []), - // Adds Anthropic cache controls when supported by the model. + ]; + const defaultMiddlewareOverrides = customMiddleware.filter((candidate) => + subagentCoreMiddleware.some( + (middleware) => middleware.name === candidate.name, + ), + ); + const subagentDefaultsWithParentOverrides = mergeMiddleware( + subagentCoreMiddleware, + defaultMiddlewareOverrides, + ); + let subagentMiddleware = [ + ...mergeMiddleware( + subagentDefaultsWithParentOverrides, + input.middleware ?? [], + ), ...cacheMiddleware, ]; + + if (harnessProfile.excludedMiddleware.size > 0) { + subagentMiddleware = subagentMiddleware.filter( + (middleware) => !harnessProfile.excludedMiddleware.has(middleware.name), + ); + } + return { ...input, tools: input.tools ?? [], @@ -365,9 +404,8 @@ export function createDeepAgent< patchToolCallsMiddleware, ] = builtInMiddleware; - // Runtime middleware array: combine built-in + optional middleware. - // Note: The full type is handled separately via AllMiddleware. - const middleware: AnyAgentMiddleware[] = [ + // Runtime middleware array: combine core middleware, custom overrides, and tail middleware. + const coreMiddleware: AgentMiddleware[] = [ // Built-in middleware with deterministic ordering. todoMiddleware, // Optional root-level skills. @@ -380,8 +418,10 @@ export function createDeepAgent< ...(asyncSubAgents.length > 0 ? [createAsyncSubAgentMiddleware({ asyncSubAgents })] : []), - // User-provided middleware. - ...customMiddleware, + ]; + const tailMiddleware: AgentMiddleware[] = [ + // Profile middleware runs before cache middleware so it participates in prompt caching. + ...resolveMiddleware(harnessProfile.extraMiddleware), // Optional Anthropic cache controls. ...cacheMiddleware, // Optional memory support. @@ -398,44 +438,22 @@ export function createDeepAgent< ...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []), ]; - // Apply profile middleware additions. Inserted before cache middleware - // so profile-injected middleware participates in prompt caching. - const profileMiddleware = resolveMiddleware(harnessProfile.extraMiddleware); - if (profileMiddleware.length > 0) { - const cacheIdx = middleware.findIndex( - (m) => m.name === "AnthropicPromptCachingMiddleware", - ); - if (cacheIdx !== -1) { - middleware.splice(cacheIdx, 0, ...profileMiddleware); - } else { - middleware.push(...profileMiddleware); - } - } + let middleware: AgentMiddleware[] = [ + ...mergeMiddleware(coreMiddleware, customMiddleware), + ...tailMiddleware, + ]; - // Apply profile middleware exclusions. + // Apply profile middleware exclusions after custom replacement so exclusions win. if (harnessProfile.excludedMiddleware.size > 0) { const excluded = harnessProfile.excludedMiddleware; - const filtered = middleware.filter((m) => !excluded.has(m.name)); - middleware.length = 0; - middleware.push(...filtered); + middleware = middleware.filter((entry) => !excluded.has(entry.name)); } // Apply profile tool exclusions via a filtering middleware that runs // after all tool-injecting middleware. if (harnessProfile.excludedTools.size > 0) { - const excludedTools = harnessProfile.excludedTools; middleware.push( - createMiddleware({ - name: "_ToolExclusionMiddleware", - wrapModelCall: async (request: any, handler: any) => { - return handler({ - ...request, - tools: request.tools?.filter( - (t: { name: string }) => !excludedTools.has(t.name), - ), - }); - }, - }), + createToolExclusionMiddleware(harnessProfile.excludedTools), ); } diff --git a/libs/deepagents/src/middleware/subagent.test.ts b/libs/deepagents/src/middleware/subagent.test.ts index ce1b8a5d8..3787891ed 100644 --- a/libs/deepagents/src/middleware/subagent.test.ts +++ b/libs/deepagents/src/middleware/subagent.test.ts @@ -27,11 +27,14 @@ import type { LangSmithTracingClientInterface } from "langsmith"; import type { Serialized } from "@langchain/core/load/serializable"; import type { ChainValues } from "@langchain/core/utils/types"; -import { createDeepAgent } from "../agent.js"; +import { mergeMiddleware, createDeepAgent } from "../agent.js"; +import { StateBackend } from "../backends/state.js"; import { createSkillsMiddleware } from "./skills.js"; +import { createSummarizationMiddleware } from "./summarization.js"; import { createFileData } from "../backends/utils.js"; import { createMockBackend } from "./test.js"; import { createSubAgent } from "./subagents.js"; +import { registerHarnessProfile } from "../profiles/index.js"; const createAgentMock = vi.mocked(createAgent); @@ -1229,3 +1232,230 @@ describe("createSubAgent", () => { expect(call.responseFormat).toBeUndefined(); }); }); + +describe("middleware override by name", () => { + const fakeModel = new FakeListChatModel({ responses: ["hello"] }); + + function namedMiddleware(name: string): AgentMiddleware { + return { name } as AgentMiddleware; + } + + function createCustomSummarizationMiddleware(): AgentMiddleware { + return createSummarizationMiddleware({ backend: new StateBackend() }); + } + + function getCreateAgentCall(name: string) { + const call = createAgentMock.mock.calls + .map(([params]) => params) + .find((params) => params.name === name); + if (call == null) { + throw new Error( + `Expected createAgent call for ${name}; saw ${createAgentMock.mock.calls + .map(([params]) => params.name ?? "") + .join(", ")}`, + ); + } + return call; + } + + function getMiddlewareStack(name: string): AgentMiddleware[] { + return getCreateAgentCall(name).middleware as AgentMiddleware[]; + } + + beforeEach(() => { + createAgentMock.mockClear(); + }); + + it("replaces matching middleware by name in place", () => { + const first = namedMiddleware("first"); + const original = namedMiddleware("target"); + const last = namedMiddleware("last"); + const replacement = namedMiddleware("target"); + + const merged = mergeMiddleware([first, original, last], [replacement]); + + expect(merged).toEqual([first, replacement, last]); + }); + + it("appends novel middleware after the base stack", () => { + const core = namedMiddleware("core"); + const customA = namedMiddleware("customA"); + const customB = namedMiddleware("customB"); + + const merged = mergeMiddleware([core], [customA, customB]); + + expect(merged).toEqual([core, customA, customB]); + }); + + it("uses the last same-name custom middleware as the replacement", () => { + const original = namedMiddleware("target"); + const first = namedMiddleware("target"); + const second = namedMiddleware("target"); + + const merged = mergeMiddleware([original], [first, second]); + + expect(merged).toEqual([second]); + }); + + it("replaces default main-agent middleware with same-name custom middleware", () => { + const custom = createCustomSummarizationMiddleware(); + + createDeepAgent({ model: fakeModel, name: "main", middleware: [custom] }); + + const middleware = getMiddlewareStack("main"); + const summarization = middleware.filter( + (entry) => entry.name === "SummarizationMiddleware", + ); + expect(summarization).toHaveLength(1); + expect(summarization[0]).toBe(custom); + }); + + it("keeps novel main-agent middleware before prompt caching", () => { + const anthropicModel = new FakeListChatModel({ responses: ["hello"] }); + vi.spyOn(anthropicModel, "getName").mockReturnValue("ChatAnthropic"); + const custom = namedMiddleware("CustomPromptMiddleware"); + + createDeepAgent({ + model: anthropicModel, + name: "main", + middleware: [custom], + }); + + const middleware = getMiddlewareStack("main"); + const customIndex = middleware.indexOf(custom); + const cacheIndex = middleware.findIndex( + (entry) => + entry.name === "AnthropicPromptCachingMiddleware" || + entry.name === "CacheBreakpointMiddleware", + ); + expect(customIndex).toBeGreaterThanOrEqual(0); + expect(cacheIndex).toBeGreaterThanOrEqual(0); + expect(customIndex).toBeLessThan(cacheIndex); + }); + + it("lets profile middleware exclusions win over custom replacements", () => { + registerHarnessProfile("override-test:model", { + excludedMiddleware: ["SummarizationMiddleware"], + }); + const custom = createCustomSummarizationMiddleware(); + + createDeepAgent({ + model: "override-test:model", + name: "main", + middleware: [custom], + }); + + const middleware = getMiddlewareStack("main"); + expect( + middleware.some((entry) => entry.name === "SummarizationMiddleware"), + ).toBe(false); + }); + + it("keeps tool exclusion middleware last", () => { + registerHarnessProfile("tool-exclusion-test:model", { + excludedTools: ["write_file"], + }); + const custom = namedMiddleware("CustomToolMiddleware"); + + createDeepAgent({ + model: "tool-exclusion-test:model", + name: "main", + middleware: [custom], + }); + + const middleware = getMiddlewareStack("main"); + expect(middleware[middleware.length - 1]?.name).toBe( + "_ToolExclusionMiddleware", + ); + }); + + it("passes main-agent default overrides to the general-purpose subagent", () => { + const custom = createCustomSummarizationMiddleware(); + + createDeepAgent({ model: fakeModel, name: "main", middleware: [custom] }); + + const middleware = getMiddlewareStack("general-purpose"); + const summarization = middleware.filter( + (entry) => entry.name === "SummarizationMiddleware", + ); + expect(summarization).toHaveLength(1); + expect(summarization[0]).toBe(custom); + }); + + it("does not pass parent-only middleware to the general-purpose subagent", () => { + const custom = namedMiddleware("ParentOnlyMiddleware"); + + createDeepAgent({ model: fakeModel, name: "main", middleware: [custom] }); + + const middleware = getMiddlewareStack("general-purpose"); + expect(middleware).not.toContain(custom); + }); + + it("passes main-agent default overrides to declarative subagents", () => { + const custom = createCustomSummarizationMiddleware(); + + createDeepAgent({ + model: fakeModel, + name: "main", + middleware: [custom], + subagents: [ + { + name: "helper", + description: "Helps with work", + systemPrompt: "Help.", + }, + ], + }); + + const middleware = getMiddlewareStack("helper"); + const summarization = middleware.filter( + (entry) => entry.name === "SummarizationMiddleware", + ); + expect(summarization).toHaveLength(1); + expect(summarization[0]).toBe(custom); + }); + + it("does not pass parent-only middleware to declarative subagents", () => { + const custom = namedMiddleware("ParentOnlyMiddleware"); + + createDeepAgent({ + model: fakeModel, + name: "main", + middleware: [custom], + subagents: [ + { + name: "helper", + description: "Helps with work", + systemPrompt: "Help.", + }, + ], + }); + + const middleware = getMiddlewareStack("helper"); + expect(middleware).not.toContain(custom); + }); + + it("replaces declarative subagent defaults with same-name spec middleware", () => { + const custom = createCustomSummarizationMiddleware(); + + createDeepAgent({ + model: fakeModel, + name: "main", + subagents: [ + { + name: "helper", + description: "Helps with work", + systemPrompt: "Help.", + middleware: [custom], + }, + ], + }); + + const middleware = getMiddlewareStack("helper"); + const summarization = middleware.filter( + (entry) => entry.name === "SummarizationMiddleware", + ); + expect(summarization).toHaveLength(1); + expect(summarization[0]).toBe(custom); + }); +}); diff --git a/libs/deepagents/src/middleware/tool_exclusion.ts b/libs/deepagents/src/middleware/tool_exclusion.ts new file mode 100644 index 000000000..efeff2281 --- /dev/null +++ b/libs/deepagents/src/middleware/tool_exclusion.ts @@ -0,0 +1,32 @@ +import { createMiddleware, type AgentMiddleware } from "langchain"; + +function hasToolName(tool: unknown): tool is { name: string } { + return ( + tool !== null && + typeof tool === "object" && + "name" in tool && + typeof tool.name === "string" + ); +} + +/** + * Create middleware that removes excluded tools after all tool-injecting + * middleware has had a chance to add tools to the request. + * + * @internal + */ +export function createToolExclusionMiddleware( + excludedTools: ReadonlySet, +): AgentMiddleware { + return createMiddleware({ + name: "_ToolExclusionMiddleware", + wrapModelCall(request, handler) { + return handler({ + ...request, + tools: request.tools?.filter( + (tool) => !hasToolName(tool) || !excludedTools.has(tool.name), + ), + }); + }, + }); +} From af221ff002b46aa937255651a635c847d19cf2af Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Mon, 13 Jul 2026 15:23:57 -0700 Subject: [PATCH 2/9] fix(deepagents): limit inherited middleware overrides to gp --- libs/deepagents/src/agent.ts | 35 +++++++++++-------- .../src/middleware/subagent.test.ts | 11 +++--- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index 533dc782f..88f2fe19e 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -270,7 +270,10 @@ export function createDeepAgent< * Only the general-purpose subagent inherits the main agent's skills. * If a custom subagent needs skills, it must specify its own `skills` array. */ - const normalizeSubagentSpec = (input: SubAgent): SubAgent => { + const normalizeSubagentSpec = ( + input: SubAgent, + parentDefaultOverrideCandidates: readonly AgentMiddleware[] = [], + ): SubAgent => { const effectivePermissions = input.permissions ?? permissions; // Middleware for custom subagents (does NOT include skills from main agent). @@ -295,10 +298,11 @@ export function createDeepAgent< ? [createSkillsMiddleware({ backend, sources: input.skills })] : []), ]; - const defaultMiddlewareOverrides = customMiddleware.filter((candidate) => - subagentCoreMiddleware.some( - (middleware) => middleware.name === candidate.name, - ), + const defaultMiddlewareOverrides = parentDefaultOverrideCandidates.filter( + (candidate) => + subagentCoreMiddleware.some( + (middleware) => middleware.name === candidate.name, + ), ); const subagentDefaultsWithParentOverrides = mergeMiddleware( subagentCoreMiddleware, @@ -355,15 +359,18 @@ export function createDeepAgent< gpConfig?.systemPrompt ?? applyProfilePrompt(harnessProfile, GENERAL_PURPOSE_SUBAGENT.systemPrompt); - const generalPurposeSpec = normalizeSubagentSpec({ - ...GENERAL_PURPOSE_SUBAGENT, - description: - gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description, - systemPrompt: gpSystemPrompt, - model, - skills, - tools: effectiveTools, - }); + const generalPurposeSpec = normalizeSubagentSpec( + { + ...GENERAL_PURPOSE_SUBAGENT, + description: + gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description, + systemPrompt: gpSystemPrompt, + model, + skills, + tools: effectiveTools, + }, + customMiddleware, + ); inlineSubagents.unshift(generalPurposeSpec); } diff --git a/libs/deepagents/src/middleware/subagent.test.ts b/libs/deepagents/src/middleware/subagent.test.ts index 3787891ed..ed8ef9419 100644 --- a/libs/deepagents/src/middleware/subagent.test.ts +++ b/libs/deepagents/src/middleware/subagent.test.ts @@ -1391,7 +1391,7 @@ describe("middleware override by name", () => { expect(middleware).not.toContain(custom); }); - it("passes main-agent default overrides to declarative subagents", () => { + it("does not pass main-agent default overrides to declarative subagents", () => { const custom = createCustomSummarizationMiddleware(); createDeepAgent({ @@ -1408,11 +1408,10 @@ describe("middleware override by name", () => { }); const middleware = getMiddlewareStack("helper"); - const summarization = middleware.filter( - (entry) => entry.name === "SummarizationMiddleware", - ); - expect(summarization).toHaveLength(1); - expect(summarization[0]).toBe(custom); + expect(middleware).not.toContain(custom); + expect( + middleware.some((entry) => entry.name === "SummarizationMiddleware"), + ).toBe(true); }); it("does not pass parent-only middleware to declarative subagents", () => { From aad5755a6f8a1267dfbaec0cb5c259d1d00239cf Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Mon, 13 Jul 2026 15:49:42 -0700 Subject: [PATCH 3/9] refactor(deepagents): pass gp overrides through spec --- libs/deepagents/src/agent.ts | 53 +++++++++++++++--------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index 88f2fe19e..0a693495c 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -270,16 +270,13 @@ export function createDeepAgent< * Only the general-purpose subagent inherits the main agent's skills. * If a custom subagent needs skills, it must specify its own `skills` array. */ - const normalizeSubagentSpec = ( - input: SubAgent, - parentDefaultOverrideCandidates: readonly AgentMiddleware[] = [], - ): SubAgent => { + const normalizeSubagentSpec = (input: SubAgent): SubAgent => { const effectivePermissions = input.permissions ?? permissions; // Middleware for custom subagents (does NOT include skills from main agent). // Uses createSummarizationMiddleware (deepagents version) with backend support // and auto-computed defaults from model profile. - const subagentCoreMiddleware = [ + const subagentDefaultMiddleware = [ // Provides todo list management capabilities for tracking tasks. todoListMiddleware(), // Enables filesystem operations and optional long-term memory storage. @@ -298,21 +295,8 @@ export function createDeepAgent< ? [createSkillsMiddleware({ backend, sources: input.skills })] : []), ]; - const defaultMiddlewareOverrides = parentDefaultOverrideCandidates.filter( - (candidate) => - subagentCoreMiddleware.some( - (middleware) => middleware.name === candidate.name, - ), - ); - const subagentDefaultsWithParentOverrides = mergeMiddleware( - subagentCoreMiddleware, - defaultMiddlewareOverrides, - ); let subagentMiddleware = [ - ...mergeMiddleware( - subagentDefaultsWithParentOverrides, - input.middleware ?? [], - ), + ...mergeMiddleware(subagentDefaultMiddleware, input.middleware ?? []), ...cacheMiddleware, ]; @@ -359,18 +343,25 @@ export function createDeepAgent< gpConfig?.systemPrompt ?? applyProfilePrompt(harnessProfile, GENERAL_PURPOSE_SUBAGENT.systemPrompt); - const generalPurposeSpec = normalizeSubagentSpec( - { - ...GENERAL_PURPOSE_SUBAGENT, - description: - gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description, - systemPrompt: gpSystemPrompt, - model, - skills, - tools: effectiveTools, - }, - customMiddleware, - ); + const gpDefaultMiddlewareNames = new Set([ + "todoListMiddleware", + "FilesystemMiddleware", + "SummarizationMiddleware", + "patchToolCallsMiddleware", + ...(skills != null && skills.length > 0 ? ["SkillsMiddleware"] : []), + ]); + const generalPurposeSpec = normalizeSubagentSpec({ + ...GENERAL_PURPOSE_SUBAGENT, + description: + gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description, + systemPrompt: gpSystemPrompt, + model, + skills, + tools: effectiveTools, + middleware: customMiddleware.filter((middleware) => + gpDefaultMiddlewareNames.has(middleware.name), + ), + }); inlineSubagents.unshift(generalPurposeSpec); } From a16285f4c373d5370804b3e6800ee9004552e0a4 Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Tue, 14 Jul 2026 17:36:42 -0700 Subject: [PATCH 4/9] refactor(deepagents): derive gp middleware override names --- libs/deepagents/src/agent.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index 0a693495c..928d0669a 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -270,13 +270,15 @@ export function createDeepAgent< * Only the general-purpose subagent inherits the main agent's skills. * If a custom subagent needs skills, it must specify its own `skills` array. */ - const normalizeSubagentSpec = (input: SubAgent): SubAgent => { + const createSubagentDefaultMiddleware = ( + input: SubAgent, + ): AgentMiddleware[] => { const effectivePermissions = input.permissions ?? permissions; // Middleware for custom subagents (does NOT include skills from main agent). // Uses createSummarizationMiddleware (deepagents version) with backend support // and auto-computed defaults from model profile. - const subagentDefaultMiddleware = [ + return [ // Provides todo list management capabilities for tracking tasks. todoListMiddleware(), // Enables filesystem operations and optional long-term memory storage. @@ -295,6 +297,10 @@ export function createDeepAgent< ? [createSkillsMiddleware({ backend, sources: input.skills })] : []), ]; + }; + + const normalizeSubagentSpec = (input: SubAgent): SubAgent => { + const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input); let subagentMiddleware = [ ...mergeMiddleware(subagentDefaultMiddleware, input.middleware ?? []), ...cacheMiddleware, @@ -343,14 +349,7 @@ export function createDeepAgent< gpConfig?.systemPrompt ?? applyProfilePrompt(harnessProfile, GENERAL_PURPOSE_SUBAGENT.systemPrompt); - const gpDefaultMiddlewareNames = new Set([ - "todoListMiddleware", - "FilesystemMiddleware", - "SummarizationMiddleware", - "patchToolCallsMiddleware", - ...(skills != null && skills.length > 0 ? ["SkillsMiddleware"] : []), - ]); - const generalPurposeSpec = normalizeSubagentSpec({ + const generalPurposeInput: SubAgent = { ...GENERAL_PURPOSE_SUBAGENT, description: gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description, @@ -358,6 +357,14 @@ export function createDeepAgent< model, skills, tools: effectiveTools, + }; + const gpDefaultMiddlewareNames = new Set( + createSubagentDefaultMiddleware(generalPurposeInput).map( + (middleware) => middleware.name, + ), + ); + const generalPurposeSpec = normalizeSubagentSpec({ + ...generalPurposeInput, middleware: customMiddleware.filter((middleware) => gpDefaultMiddlewareNames.has(middleware.name), ), From 7a21fe7b278b39f609d23e1885eb959e4b8a586b Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Tue, 14 Jul 2026 17:45:06 -0700 Subject: [PATCH 5/9] refactor(deepagents): reuse normalized gp middleware --- libs/deepagents/src/agent.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index 928d0669a..9970038db 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -349,7 +349,7 @@ export function createDeepAgent< gpConfig?.systemPrompt ?? applyProfilePrompt(harnessProfile, GENERAL_PURPOSE_SUBAGENT.systemPrompt); - const generalPurposeInput: SubAgent = { + const generalPurposeSpec = normalizeSubagentSpec({ ...GENERAL_PURPOSE_SUBAGENT, description: gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description, @@ -357,18 +357,18 @@ export function createDeepAgent< model, skills, tools: effectiveTools, - }; - const gpDefaultMiddlewareNames = new Set( - createSubagentDefaultMiddleware(generalPurposeInput).map( + }); + const gpMiddlewareNames = new Set( + (generalPurposeSpec.middleware ?? []).map( (middleware) => middleware.name, ), ); - const generalPurposeSpec = normalizeSubagentSpec({ - ...generalPurposeInput, - middleware: customMiddleware.filter((middleware) => - gpDefaultMiddlewareNames.has(middleware.name), + generalPurposeSpec.middleware = mergeMiddleware( + generalPurposeSpec.middleware ?? [], + customMiddleware.filter((middleware) => + gpMiddlewareNames.has(middleware.name), ), - }); + ); inlineSubagents.unshift(generalPurposeSpec); } From 04053f43e2361351cf2dce6656801ded92dd2b8b Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Tue, 14 Jul 2026 17:57:59 -0700 Subject: [PATCH 6/9] fix(deepagents): replace tail middleware by name --- libs/deepagents/src/agent.ts | 29 +++++++++++++++++-- .../src/middleware/subagent.test.ts | 21 ++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index 9970038db..d7a7e04dc 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -443,9 +443,34 @@ export function createDeepAgent< ...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []), ]; + const coreMiddlewareNames = new Set( + coreMiddleware.map((middleware) => middleware.name), + ); + const tailMiddlewareNames = new Set( + tailMiddleware.map((middleware) => middleware.name), + ); + const defaultMiddlewareNames = new Set([ + ...coreMiddlewareNames, + ...tailMiddlewareNames, + ]); + const novelMiddleware = customMiddleware.filter( + (middleware) => !defaultMiddlewareNames.has(middleware.name), + ); + let middleware: AgentMiddleware[] = [ - ...mergeMiddleware(coreMiddleware, customMiddleware), - ...tailMiddleware, + ...mergeMiddleware( + coreMiddleware, + customMiddleware.filter((middleware) => + coreMiddlewareNames.has(middleware.name), + ), + ), + ...novelMiddleware, + ...mergeMiddleware( + tailMiddleware, + customMiddleware.filter((middleware) => + tailMiddlewareNames.has(middleware.name), + ), + ), ]; // Apply profile middleware exclusions after custom replacement so exclusions win. diff --git a/libs/deepagents/src/middleware/subagent.test.ts b/libs/deepagents/src/middleware/subagent.test.ts index ed8ef9419..7398c1433 100644 --- a/libs/deepagents/src/middleware/subagent.test.ts +++ b/libs/deepagents/src/middleware/subagent.test.ts @@ -1333,6 +1333,27 @@ describe("middleware override by name", () => { expect(customIndex).toBeLessThan(cacheIndex); }); + it("replaces prompt cache defaults in both main and general-purpose stacks", () => { + const anthropicModel = new FakeListChatModel({ responses: ["hello"] }); + vi.spyOn(anthropicModel, "getName").mockReturnValue("ChatAnthropic"); + const custom = namedMiddleware("CacheBreakpointMiddleware"); + + createDeepAgent({ + model: anthropicModel, + name: "main", + middleware: [custom], + }); + + for (const agentName of ["main", "general-purpose"]) { + const middleware = getMiddlewareStack(agentName); + const cacheEntries = middleware.filter( + (entry) => entry.name === "CacheBreakpointMiddleware", + ); + expect(cacheEntries).toHaveLength(1); + expect(cacheEntries[0]).toBe(custom); + } + }); + it("lets profile middleware exclusions win over custom replacements", () => { registerHarnessProfile("override-test:model", { excludedMiddleware: ["SummarizationMiddleware"], From f5ecb11f579fa28cc631a78d1352c1763bae2dd8 Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Tue, 14 Jul 2026 18:02:34 -0700 Subject: [PATCH 7/9] refactor(deepagents): clarify middleware replacement assembly --- libs/deepagents/src/agent.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index d7a7e04dc..c00c57b20 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -128,6 +128,17 @@ export function mergeMiddleware( return [...merged.values()]; } +function middlewareNames(middleware: readonly AgentMiddleware[]): Set { + return new Set(middleware.map((entry) => entry.name)); +} + +function matchingMiddleware( + middleware: readonly AgentMiddleware[], + names: ReadonlySet, +): AgentMiddleware[] { + return middleware.filter((entry) => names.has(entry.name)); +} + /** * Create a Deep Agent. * @@ -443,33 +454,24 @@ export function createDeepAgent< ...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []), ]; - const coreMiddlewareNames = new Set( - coreMiddleware.map((middleware) => middleware.name), - ); - const tailMiddlewareNames = new Set( - tailMiddleware.map((middleware) => middleware.name), - ); + const coreMiddlewareNames = middlewareNames(coreMiddleware); + const tailMiddlewareNames = middlewareNames(tailMiddleware); const defaultMiddlewareNames = new Set([ ...coreMiddlewareNames, ...tailMiddlewareNames, ]); - const novelMiddleware = customMiddleware.filter( - (middleware) => !defaultMiddlewareNames.has(middleware.name), - ); let middleware: AgentMiddleware[] = [ ...mergeMiddleware( coreMiddleware, - customMiddleware.filter((middleware) => - coreMiddlewareNames.has(middleware.name), - ), + matchingMiddleware(customMiddleware, coreMiddlewareNames), + ), + ...customMiddleware.filter( + (entry) => !defaultMiddlewareNames.has(entry.name), ), - ...novelMiddleware, ...mergeMiddleware( tailMiddleware, - customMiddleware.filter((middleware) => - tailMiddlewareNames.has(middleware.name), - ), + matchingMiddleware(customMiddleware, tailMiddlewareNames), ), ]; From 707d7610b9104c02673d40baaa7360154b788b04 Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Tue, 14 Jul 2026 18:23:51 -0700 Subject: [PATCH 8/9] refactor(deepagents): consolidate middleware stack merging --- libs/deepagents/src/agent.ts | 79 +++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index c00c57b20..8cfb5dca8 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -139,6 +139,38 @@ function matchingMiddleware( return middleware.filter((entry) => names.has(entry.name)); } +function mergeMiddlewareStack( + defaultMiddleware: readonly AgentMiddleware[], + customMiddleware: readonly AgentMiddleware[], + tailMiddleware: readonly AgentMiddleware[] = [], + options: { appendNew?: boolean } = {}, +): AgentMiddleware[] { + const defaultMiddlewareNames = middlewareNames(defaultMiddleware); + const tailMiddlewareNames = middlewareNames(tailMiddleware); + const knownMiddlewareNames = new Set([ + ...defaultMiddlewareNames, + ...tailMiddlewareNames, + ]); + const novelMiddleware = + options.appendNew === false + ? [] + : customMiddleware.filter( + (entry) => !knownMiddlewareNames.has(entry.name), + ); + + return [ + ...mergeMiddleware( + defaultMiddleware, + matchingMiddleware(customMiddleware, defaultMiddlewareNames), + ), + ...novelMiddleware, + ...mergeMiddleware( + tailMiddleware, + matchingMiddleware(customMiddleware, tailMiddlewareNames), + ), + ]; +} + /** * Create a Deep Agent. * @@ -312,10 +344,11 @@ export function createDeepAgent< const normalizeSubagentSpec = (input: SubAgent): SubAgent => { const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input); - let subagentMiddleware = [ - ...mergeMiddleware(subagentDefaultMiddleware, input.middleware ?? []), - ...cacheMiddleware, - ]; + let subagentMiddleware = mergeMiddlewareStack( + subagentDefaultMiddleware, + input.middleware ?? [], + cacheMiddleware, + ); if (harnessProfile.excludedMiddleware.size > 0) { subagentMiddleware = subagentMiddleware.filter( @@ -369,16 +402,11 @@ export function createDeepAgent< skills, tools: effectiveTools, }); - const gpMiddlewareNames = new Set( - (generalPurposeSpec.middleware ?? []).map( - (middleware) => middleware.name, - ), - ); - generalPurposeSpec.middleware = mergeMiddleware( + generalPurposeSpec.middleware = mergeMiddlewareStack( generalPurposeSpec.middleware ?? [], - customMiddleware.filter((middleware) => - gpMiddlewareNames.has(middleware.name), - ), + customMiddleware, + [], + { appendNew: false }, ); inlineSubagents.unshift(generalPurposeSpec); } @@ -454,26 +482,11 @@ export function createDeepAgent< ...(interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []), ]; - const coreMiddlewareNames = middlewareNames(coreMiddleware); - const tailMiddlewareNames = middlewareNames(tailMiddleware); - const defaultMiddlewareNames = new Set([ - ...coreMiddlewareNames, - ...tailMiddlewareNames, - ]); - - let middleware: AgentMiddleware[] = [ - ...mergeMiddleware( - coreMiddleware, - matchingMiddleware(customMiddleware, coreMiddlewareNames), - ), - ...customMiddleware.filter( - (entry) => !defaultMiddlewareNames.has(entry.name), - ), - ...mergeMiddleware( - tailMiddleware, - matchingMiddleware(customMiddleware, tailMiddlewareNames), - ), - ]; + let middleware: AgentMiddleware[] = mergeMiddlewareStack( + coreMiddleware, + customMiddleware, + tailMiddleware, + ); // Apply profile middleware exclusions after custom replacement so exclusions win. if (harnessProfile.excludedMiddleware.size > 0) { From 2b1c1a42d789f93a0dbe08d1584bfdfe7e4a7230 Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Tue, 14 Jul 2026 18:32:10 -0700 Subject: [PATCH 9/9] refactor(deepagents): move middleware merge helpers --- libs/deepagents/src/agent.ts | 65 +---------------- .../src/middleware/subagent.test.ts | 3 +- libs/deepagents/src/middleware/utils.ts | 70 +++++++++++++++++++ 3 files changed, 73 insertions(+), 65 deletions(-) diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index 8cfb5dca8..41fa3dff1 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -32,6 +32,7 @@ import { ConfigurationError } from "./errors.js"; import { InteropZodObject } from "@langchain/core/utils/types"; import { createCacheBreakpointMiddleware } from "./middleware/cache.js"; import { createToolExclusionMiddleware } from "./middleware/tool_exclusion.js"; +import { mergeMiddlewareStack } from "./middleware/utils.js"; import { GENERAL_PURPOSE_SUBAGENT, type CompiledSubAgent, @@ -107,70 +108,6 @@ const BUILTIN_TOOL_NAMES: ReadonlySet = new Set([ "write_todos", ]); -/** - * Merge custom middleware into an assembled stack by `.name`. - * - * Matching custom middleware replaces the existing entry in place. New - * middleware is appended after the base stack in caller-provided order. - * - * @internal - */ -export function mergeMiddleware( - base: readonly AgentMiddleware[], - custom: readonly AgentMiddleware[], -): AgentMiddleware[] { - const merged = new Map( - base.map((middleware) => [middleware.name, middleware]), - ); - for (const middleware of custom) { - merged.set(middleware.name, middleware); - } - return [...merged.values()]; -} - -function middlewareNames(middleware: readonly AgentMiddleware[]): Set { - return new Set(middleware.map((entry) => entry.name)); -} - -function matchingMiddleware( - middleware: readonly AgentMiddleware[], - names: ReadonlySet, -): AgentMiddleware[] { - return middleware.filter((entry) => names.has(entry.name)); -} - -function mergeMiddlewareStack( - defaultMiddleware: readonly AgentMiddleware[], - customMiddleware: readonly AgentMiddleware[], - tailMiddleware: readonly AgentMiddleware[] = [], - options: { appendNew?: boolean } = {}, -): AgentMiddleware[] { - const defaultMiddlewareNames = middlewareNames(defaultMiddleware); - const tailMiddlewareNames = middlewareNames(tailMiddleware); - const knownMiddlewareNames = new Set([ - ...defaultMiddlewareNames, - ...tailMiddlewareNames, - ]); - const novelMiddleware = - options.appendNew === false - ? [] - : customMiddleware.filter( - (entry) => !knownMiddlewareNames.has(entry.name), - ); - - return [ - ...mergeMiddleware( - defaultMiddleware, - matchingMiddleware(customMiddleware, defaultMiddlewareNames), - ), - ...novelMiddleware, - ...mergeMiddleware( - tailMiddleware, - matchingMiddleware(customMiddleware, tailMiddlewareNames), - ), - ]; -} - /** * Create a Deep Agent. * diff --git a/libs/deepagents/src/middleware/subagent.test.ts b/libs/deepagents/src/middleware/subagent.test.ts index 7398c1433..a2ebe85d3 100644 --- a/libs/deepagents/src/middleware/subagent.test.ts +++ b/libs/deepagents/src/middleware/subagent.test.ts @@ -27,10 +27,11 @@ import type { LangSmithTracingClientInterface } from "langsmith"; import type { Serialized } from "@langchain/core/load/serializable"; import type { ChainValues } from "@langchain/core/utils/types"; -import { mergeMiddleware, createDeepAgent } from "../agent.js"; +import { createDeepAgent } from "../agent.js"; import { StateBackend } from "../backends/state.js"; import { createSkillsMiddleware } from "./skills.js"; import { createSummarizationMiddleware } from "./summarization.js"; +import { mergeMiddleware } from "./utils.js"; import { createFileData } from "../backends/utils.js"; import { createMockBackend } from "./test.js"; import { createSubAgent } from "./subagents.js"; diff --git a/libs/deepagents/src/middleware/utils.ts b/libs/deepagents/src/middleware/utils.ts index 1612954d9..751264c2a 100644 --- a/libs/deepagents/src/middleware/utils.ts +++ b/libs/deepagents/src/middleware/utils.ts @@ -5,6 +5,76 @@ */ import { SystemMessage } from "@langchain/core/messages"; +import type { AgentMiddleware } from "langchain"; + +/** + * Merge custom middleware into an assembled stack by `.name`. + * + * Matching custom middleware replaces the existing entry in place. New + * middleware is appended after the base stack in caller-provided order. + */ +export function mergeMiddleware( + base: readonly AgentMiddleware[], + custom: readonly AgentMiddleware[], +): AgentMiddleware[] { + const merged = new Map( + base.map((middleware) => [middleware.name, middleware]), + ); + for (const middleware of custom) { + merged.set(middleware.name, middleware); + } + return [...merged.values()]; +} + +function middlewareNames(middleware: readonly AgentMiddleware[]): Set { + return new Set(middleware.map((entry) => entry.name)); +} + +function matchingMiddleware( + middleware: readonly AgentMiddleware[], + names: ReadonlySet, +): AgentMiddleware[] { + return middleware.filter((entry) => names.has(entry.name)); +} + +/** + * Merge custom middleware into default and tail middleware segments. + * + * Same-name custom entries replace matching defaults in either segment. Novel + * custom entries are inserted between the default and tail segments unless + * `appendNew` is false. + */ +export function mergeMiddlewareStack( + defaultMiddleware: readonly AgentMiddleware[], + customMiddleware: readonly AgentMiddleware[], + tailMiddleware: readonly AgentMiddleware[] = [], + options: { appendNew?: boolean } = {}, +): AgentMiddleware[] { + const defaultMiddlewareNames = middlewareNames(defaultMiddleware); + const tailMiddlewareNames = middlewareNames(tailMiddleware); + const knownMiddlewareNames = new Set([ + ...defaultMiddlewareNames, + ...tailMiddlewareNames, + ]); + const novelMiddleware = + options.appendNew === false + ? [] + : customMiddleware.filter( + (entry) => !knownMiddlewareNames.has(entry.name), + ); + + return [ + ...mergeMiddleware( + defaultMiddleware, + matchingMiddleware(customMiddleware, defaultMiddlewareNames), + ), + ...novelMiddleware, + ...mergeMiddleware( + tailMiddleware, + matchingMiddleware(customMiddleware, tailMiddlewareNames), + ), + ]; +} /** * Append text to a system message.