Skip to content
5 changes: 5 additions & 0 deletions .changeset/soft-zebras-merge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"deepagents": patch
---

fix(deepagents): allow custom middleware to replace defaults by name
86 changes: 43 additions & 43 deletions libs/deepagents/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import {
createAgent,
createMiddleware,
humanInTheLoopMiddleware,
anthropicPromptCachingMiddleware,
bedrockPromptCachingMiddleware,
todoListMiddleware,
SystemMessage,
type AgentMiddleware,
type AnyAgentMiddleware,
context,
} from "langchain";
import type {
Expand All @@ -33,6 +31,8 @@ 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 { mergeMiddlewareStack } from "./middleware/utils.js";
import {
GENERAL_PURPOSE_SUBAGENT,
type CompiledSubAgent,
Expand Down Expand Up @@ -223,7 +223,7 @@ export function createDeepAgent<

const anthropicModel = isAnthropicModel(model);
const bedrockModel = isBedrockConverseModel(model);
let cacheMiddleware: AnyAgentMiddleware[] = [];
let cacheMiddleware: AgentMiddleware[] = [];

if (anthropicModel) {
cacheMiddleware = [
Expand All @@ -250,13 +250,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 subagentMiddleware = [
return [
// Provides todo list management capabilities for tracking tasks.
todoListMiddleware(),
// Enables filesystem operations and optional long-term memory storage.
Expand All @@ -274,11 +276,23 @@ 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.
...cacheMiddleware,
];
};

const normalizeSubagentSpec = (input: SubAgent): SubAgent => {
const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input);
let subagentMiddleware = mergeMiddlewareStack(
subagentDefaultMiddleware,
input.middleware ?? [],
cacheMiddleware,
);

if (harnessProfile.excludedMiddleware.size > 0) {
subagentMiddleware = subagentMiddleware.filter(
(middleware) => !harnessProfile.excludedMiddleware.has(middleware.name),
);
}

return {
...input,
tools: input.tools ?? [],
Expand Down Expand Up @@ -325,6 +339,12 @@ export function createDeepAgent<
skills,
tools: effectiveTools,
});
generalPurposeSpec.middleware = mergeMiddlewareStack(
generalPurposeSpec.middleware ?? [],
customMiddleware,
[],
{ appendNew: false },
);
inlineSubagents.unshift(generalPurposeSpec);
}

Expand Down Expand Up @@ -365,9 +385,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.
Expand All @@ -380,8 +399,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.
Expand All @@ -398,44 +419,23 @@ 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[] = mergeMiddlewareStack(
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),
);
}

Expand Down
Loading
Loading