The agent loop¶
DeepAgents implements a ReAct (Reason + Act) loop: the model reasons about a task, calls tools, observes results, and continues until the task is complete or the iteration cap is hit. ReactAgent is the concrete type that owns this loop.
createAgent vs createDeepAgent¶
Both factories return a ReactAgent. Choose based on how much structure you need.
createAgent - minimal, bring your own¶
public func createAgent(
model: any ChatModel,
tools: [any AgentTool] = [],
systemPrompt: String? = nil,
middleware: [any AgentMiddleware] = [],
memory: (any AgentCheckpointer)? = nil,
maxIterations: Int = 24,
disabledToolNames: Set<String> = [],
messageLog: (any AgentMessageLog)? = nil
) -> ReactAgent
Use createAgent when you want full manual control: you supply every tool and every middleware yourself. The factory merges the tools array with tools contributed by each middleware, then filters out any names in disabledToolNames.
createDeepAgent - batteries-included¶
public func createDeepAgent(
model: any ChatModel,
tools: [any AgentTool] = [],
systemPrompt: String? = nil,
subagents: [SubAgent] = [],
middleware: [any AgentMiddleware] = [],
memory: (any AgentCheckpointer)? = nil,
backend: (any FilesystemBackend)? = nil,
interruptOn: [String: InterruptOnConfig] = [:],
approvalHandler: ToolApprovalHandler? = nil,
askUserHandler: AskUserHandler? = nil,
includeFilesystem: Bool = true,
includeGeneralPurpose: Bool = true,
maxIterations: Int = 24,
disabledToolNames: Set<String> = [],
messageLog: (any AgentMessageLog)? = nil,
summarization: SummarizationConfig? = .default
) -> ReactAgent
Use createDeepAgent when you want the full structural stack out of the box. It composes middleware in a fixed, intentional order:
TodoListMiddleware- planning discipline; contributeswrite_todosFilesystemMiddleware- file I/O via the suppliedbackend(defaults toStateBackendwhenincludeFilesystemis true)SubAgentMiddleware- delegation via thetasktool; wires upsubagents- Your additional
middleware(inserted here, in the order you provide) AskUserMiddleware- lets the model pause and ask the user a question (only whenaskUserHandler != nil)HumanInTheLoopMiddleware- approval gating on every tool call (only whenapprovalHandler != nil)SummarizationMiddleware- automatic context compaction (whensummarization != nil)
includeGeneralPurpose: true adds the web, search, text, git, and shell capability middleware. See Middleware for the full capability catalog.
One ReAct round - step by step¶
A single run of agent.run(...) may span many rounds. Each round is:
┌─────────────────────────────────────────────┐
│ 1. beforeModel middleware hooks fire │
│ 2. session.nextTurn(messages, tools, ...) │ ← full conversation rebuilt from scratch
│ streaming tokens → onEvent(.token(...)) │
│ 3. afterModel middleware hooks fire │
│ 4. assistant turn appended to thread │
│ 5. If no tool calls → done (final answer) │
│ 6. duplicate-round guard check │
│ 7. For each tool call (in parallel): │
│ wrapToolCall nest → execute │
│ onEvent(.toolStarted / .toolCompleted)│
│ 8. Tool result messages appended │
│ 9. Repeat from step 1 │
└─────────────────────────────────────────────┘
The model turn¶
ReactAgent creates one ModelTurnSession per run(...) call. Each round, it calls:
session.nextTurn(
messages: [AgentMessage], // full thread rebuilt every round
systemPrompt: String?,
tools: [any AgentTool],
onChunk: ...
)
The session is stateless from the agent's perspective: the full conversation history is passed in every time. This matters because any middleware that rewrites messages in beforeModel or wrapModelCall sees a complete, consistent view each round.
Tool dispatch¶
When the model returns an assistant message containing tool calls, the loop:
- Passes each call through the
wrapToolCallmiddleware nest (first-registered middleware is outermost). - Calls
tool.execute(arguments, context)on the appropriateAgentTool. - Appends a
.toolrole message for each result. - Emits
AgentEvent.toolStartedbefore andAgentEvent.toolCompletedafter each call.
The duplicate-round guard¶
Before dispatching tool calls for a round, the loop compares the tool call set against the previous round. If the model emits the exact same calls again (identical names and arguments), the round is skipped to prevent infinite loops in cases where a tool keeps returning the same output.
maxIterations and the forced final answer¶
If the loop reaches maxIterations without the model producing a no-tool turn, the agent forces one final call to the model with tools stripped from the prompt. This guarantees the agent always produces a human-readable answer rather than silently stopping.
ReactAgent - the surface you call¶
public struct ReactAgent: Sendable {
public func run(
_ input: [AgentMessage],
threadId: String? = nil,
onEvent: @Sendable @escaping (AgentEvent) -> Void
) async -> Bool
public var contextWindowTokens: Int?
@discardableResult
public func compact(threadId: String?) async -> CompactionOutcome?
}
run(...)returnstrueon success,falseon failure. Failures are also delivered viaonEvent(.failed(error)).threadIdkeys the conversation in theAgentCheckpointer(memory). Passing the samethreadIdacross calls gives the agent short-term memory of prior turns.compact(threadId:)triggers manual context compaction for a thread. NormallySummarizationMiddlewarehandles this automatically, but you can call it explicitly - for example, before archiving a conversation.
The AgentEvent stream¶
The onEvent closure receives a stream of typed events as the run progresses. The principal cases are:
| Event | When it fires |
|---|---|
.token(text, ...) |
A streamed token from the model arrives |
.toolStarted(name, input) |
A tool call is about to be dispatched |
.toolCompleted(name, result, ...) |
A tool call has returned |
.completed |
The run finished successfully |
.failed(error) |
The run failed; error is attached |
Note
The onEvent closure is @Sendable and may be called from a non-main actor context. If you're updating UI, dispatch to the main actor inside the closure.
Use the event stream to drive progress indicators, log tool usage, stream assistant tokens to a chat UI, or observe costs and timing.
Message logging¶
Pass a messageLog: conformer to either factory to record every message the agent appends. JSONLMessageLog writes one JSON line per message to a timestamped file - useful for debugging and audit trails. See Messages & content for the AgentMessageLog protocol.
Related pages¶
- Messages & content -
AgentMessage, roles, content blocks, andAgentJSON - Middleware - hook order, built-in middleware, capability catalog
- Summarization - automatic context compaction