Middleware¶
Middleware is DeepAgents' primary extension point. Every middleware observes and can rewrite the full agent lifecycle - messages, system prompts, tool lists, and individual tool calls. The same mechanism powers DeepAgents' built-in structural pillars (planning, filesystem, subagents, summarization, human-in-the-loop) and the swappable capability toolsets (web, git, shell, macOS, ...).
AgentMiddleware protocol¶
public protocol AgentMiddleware: Sendable {
var name: String { get }
var tools: [any AgentTool] { get }
func beforeAgent(_ state: inout AgentState) async
func beforeModel(_ state: inout AgentState) async
func afterModel(_ state: inout AgentState) async
func afterAgent(_ state: inout AgentState) async
func wrapModelCall(
_ request: ModelRequest,
_ handler: (ModelRequest) async throws -> ModelResponse
) async throws -> ModelResponse
func wrapToolCall(
_ request: ToolCallRequest,
_ handler: (ToolCallRequest) async throws -> AgentMessage
) async throws -> AgentMessage
}
Hook execution order¶
run(...) called
│
├── beforeAgent (all middleware, once per run, in registration order)
│
│ ┌── [Round N] ───────────────────────────────────────────────────┐
│ │ beforeModel (all middleware, every round, registration order) │
│ │ │
│ │ wrapModelCall ← NESTED, first-registered is OUTERMOST │
│ │ └── session.nextTurn(...) │
│ │ /wrapModelCall │
│ │ │
│ │ afterModel (all middleware, every round, registration order) │
│ │ │
│ │ For each tool call: │
│ │ wrapToolCall ← NESTED, first-registered is OUTERMOST │
│ │ └── tool.execute(...) │
│ │ /wrapToolCall │
│ └── [End round] ────────────────────────────────────────────────┘
│
└── afterAgent (all middleware, once per run, reverse order)
Nesting vs. sequencing
wrapModelCall and wrapToolCall are nested decorators - each middleware wraps the next, exactly like HTTP middleware stacks. The middleware registered first is the outermost layer: it can inspect and modify the request before any inner middleware sees it, and it sees the final response after all inner middleware have processed it. By contrast, beforeModel/afterModel are sequential callbacks that each mutate shared AgentState.
What each hook can do¶
| Hook | When | What you can do |
|---|---|---|
beforeAgent |
Once at run start | Inject system prompt additions, initialise state, log run start |
beforeModel |
Before each model call | Rewrite messages, edit systemPrompt, add/remove tools |
afterModel |
After each model call | Inspect the just-produced assistant message, update state |
afterAgent |
Once at run end | Flush logs, release resources, report metrics |
wrapModelCall |
Around the model call | Intercept/retry the model call; rewrite request (messages, tools, prompt) |
wrapToolCall |
Around each tool call | Intercept/approve/deny/retry a tool call; rewrite arguments or result |
AgentState holds the live conversation thread, system prompt, and tool list for the current round. Mutations in beforeModel are visible to the model call that follows.
Contributing tools¶
The tools property lets middleware own the tools it provides. Tools contributed this way are merged with the explicit tools: array at factory time, then filtered through disabledToolNames. Middleware should declare all the tools it manages via this property rather than injecting them through beforeModel - this ensures correct deduplication and policy enforcement.
Structural pillars¶
Structural middleware implements the core agent architecture. createDeepAgent wires these in automatically; createAgent leaves them out unless you add them yourself.
TodoListMiddleware¶
Adds planning discipline to the agent. Appends writing guidance to the system prompt and contributes the write_todos tool, which lets the model maintain a structured task list that persists across rounds within a run.
FilesystemMiddleware¶
Provides file I/O through a pluggable FilesystemBackend. Two backends ship:
StateBackend- in-memory; good for tests and isolated runsLocalFilesystemBackend- reads and writes real files on disk
Tools: ls, read_file, write_file, edit_file, mkdir. Pass a backend via createDeepAgent's backend: parameter; it defaults to StateBackend() when includeFilesystem: true.
SubAgentMiddleware¶
Enables task delegation. Contributes the task tool; when the model calls task, the middleware routes execution to one of the registered SubAgent instances (or to a general-purpose sub-agent when includeGeneralPurpose: true). See Subagents.
SummarizationMiddleware¶
Hooks beforeModel and compacts the conversation when the context window reaches ~85% of capacity. Summarised segments are replaced with a single synthetic message whose source field is set to identify it as compaction-synthesised. Configured via SummarizationConfig (pass nil to disable). See Summarization.
HumanInTheLoopMiddleware¶
Hooks wrapToolCall and calls your ToolApprovalHandler before every tool execution. The handler can approve, deny, or ask for user confirmation. Required for tools with side effects (file writes, shell commands, macOS automation). See Human in the loop.
AskUserMiddleware¶
Contributes the ask_user tool. When the model calls ask_user, execution suspends until your AskUserHandler returns a string. This lets the model request clarification mid-run without ending the run.
Capability catalog¶
Capability middleware provides toolsets that map cleanly to a single concern. All of these live in MiddlewareCatalog.all and can be enabled by ID. createDeepAgent with includeGeneralPurpose: true adds web, search, text, git, and shell automatically.
| Middleware ID | Type | Tools contributed |
|---|---|---|
web |
WebToolsMiddleware |
fetch, curl |
search |
SearchToolsMiddleware |
grep, glob, tree |
text |
TextToolsMiddleware |
head, tail, diff |
git |
GitToolsMiddleware |
git_status, git_diff, git_log, git_show, git_blame |
shell |
ShellToolsMiddleware |
shell (gated by ShellGuard) |
macos |
MacToolsMiddleware |
mdfind, open, open_app, download, say, notify |
filesystem |
FilesystemMiddleware |
ls, read_file, write_file, edit_file, mkdir |
clipboard |
ClipboardMiddleware |
read_clipboard, write_clipboard |
screenshot |
ScreenshotMiddleware |
take_screenshot, take_window_screenshots |
apple_notes |
AppleNotesMiddleware |
list_notes, read_note, create_note, update_note |
container |
ContainerShellMiddleware |
container_shell (sandbox mode) |
macOS adapter required
screenshot, clipboard, apple_notes, macos, and container are provided by the DeepAgentsMacTools product and require macOS entitlements. Import DeepAgentsMacTools separately.
createDeepAgent composition order¶
When you call createDeepAgent, middleware is assembled in this order before being handed to ReactAgent:
TodoListMiddlewareFilesystemMiddleware(whenincludeFilesystem: true)SubAgentMiddleware(whensubagentsis non-empty orincludeGeneralPurpose: true)- Your
middlewarearray (in the order you provide) AskUserMiddleware(whenaskUserHandler != nil)HumanInTheLoopMiddleware(whenapprovalHandler != nil)SummarizationMiddleware(whensummarization != nil)
Because wrapToolCall nests with the first-registered middleware outermost, HumanInTheLoopMiddleware always wraps the outermost layer of tool dispatch - meaning approval fires before any inner middleware can execute the call. Summarization hooks beforeModel, so it runs last in that phase and can compact the history produced by all prior middleware.
Disabling middleware and tools¶
Via disabledToolNames¶
Both factories accept disabledToolNames: Set<String>. Any tool whose name appears in this set is removed from the merged tool list at factory time. The model never sees the tool in its schema - it cannot call something that was never offered.
Via AgentToolPolicy¶
public struct AgentToolPolicy: Codable, Sendable {
public var disabledMiddleware: Set<String> // middleware IDs (catalog names)
public var disabledTools: Set<String> // individual tool names
public var approvals: [String: ToolApprovalMode]
public var sandbox: SandboxMode
public var sandboxImage: String?
public func expand(
catalog: [MiddlewareDescriptor] = MiddlewareCatalog.all,
extraDefaults: [String: ToolApprovalMode] = [:]
) -> Expansion
}
AgentToolPolicy is a serialisable value (Codable) useful for per-user or per-session configuration - for example, persisting the user's approval preferences between sessions in Ripple. Call expand(...) to resolve the policy against the live catalog and get back a concrete set of tools to disable and approval modes to apply.
Disabling is at factory time
Tools removed via disabledMiddleware or disabledTools are never rendered into the model's prompt. This is architecturally different from the approval gate, which fires at dispatch time. Disabled tools cost zero tokens and cannot be called even accidentally; approval-gated tools appear in the prompt but require explicit authorisation before execution.
Related pages¶
- Tools & policy -
AgentToolprotocol,ToolParameter,AgentToolPolicyin detail - Subagents -
SubAgentMiddlewareand thetasktool - Human in the loop -
HumanInTheLoopMiddlewareand approval handlers - Summarization -
SummarizationMiddlewareandSummarizationConfig - Write custom middleware - implementing
AgentMiddlewareyourself