Agent App Architecture#
The standalone Agent app (AppMode.AGENT, value "agent") is Dify's next-generation agent surface. It differs from the legacy AGENT_CHAT mode in two key ways: (1) its configuration comes from a versioned AgentSoulConfig JSON snapshot stored per-version in AgentConfigVersion.config_snapshot, not from a flat AppModelConfig row; and (2) execution is fully delegated to the dify-agent FastAPI microservice (Agent V2 backend) rather than running an in-process ReAct or function-calling loop .
The Agent app reuses Dify's existing EasyUI chat pipeline β conversation/message persistence, SSE streaming, annotation replies, content moderation β through a thin adapter layer. The agent backend produces the answer; the standard pipeline delivers and persists it. See also: Agent V2 Architecture for the backend internals.
HTTP request
β
βΌ
AppGenerateService._dispatch_generate() β routes AppMode.AGENT
β
βΌ
AgentAppGenerator.generate() β api/core/app/apps/agent_app/app_generator.py
βββ _resolve_agent() β loads AgentSoulConfig snapshot/draft
βββ AgentAppConfigManager β synthesizes EasyUI AppConfig for pipeline
βββ MessageBasedAppQueueManager β SSE queue
βββ worker thread β AgentAppRunner.run()
βββ AgentAppWorkspaceStore β load/create AgentWorkspaceBinding
βββ AgentAppRuntimeRequestBuilder β builds CreateRunRequest
βββ agent_backend_client.create_run() β dify-agent service
βββ _consume_stream() β adapts backend SSE to queue events
βββ _save_session() β saves CompositorSessionSnapshot to binding
Entry Point and Config Resolution#
AgentAppGenerator.generate() is the entry point. It only supports streaming mode and orchestrates the following steps:
-
Resolve agent + soul:
_resolve_agent()loads the correctAgentSoulConfigbased on invoke context:- DEBUGGER invokes: resolves an
AgentConfigDraft(typeDRAFTorDEBUG_BUILD) - Published runs (new conversation): uses
agent.active_config_snapshot_id - Existing conversations: reads
conversation.agent_workspace_binding_idβAgentWorkspaceBinding.agent_config_version_idto guarantee the conversation always uses the config it started with, even after a publish
- DEBUGGER invokes: resolves an
-
Synthesize EasyUI config:
AgentAppConfigManager.get_app_config()converts theAgentSoulConfiginto anAgentAppConfig(anEasyUIBasedAppConfigsubclass) so the downstream chat pipeline can handle usage persistence, feature flags, and prompt config identically to a regular chat app. -
Initialize records:
_init_generate_records()creates theConversationandMessageDB rows, then spawns a worker thread that calls_generate_worker(). -
Input guards (in worker, skipped on resume): content moderation and annotation reply checks run before the backend call. An annotation hit publishes the answer and returns early without reaching the agent backend .
The agent_session_scope_config_version_id field (passed as session_scope_snapshot_id to the runner) scopes workspace binding reuse: draft runs use the draft row ID; published runs use the immutable snapshot ID. This prevents state leakage between draft/published surfaces .
Session State: Workspace Binding and Session Snapshots#
Multi-turn memory is managed through AgentAppWorkspaceStore, which resolves a persistent AgentWorkspaceBinding that connects the conversation (or build draft) to the agent's workspace.
Dual persistence: each conversation turn writes to two places:
- Standard Dify tables (
Conversation,Message,MessageAgentThought): handled by the EasyUI pipeline as usual. AgentWorkspaceBinding.session_snapshot: a serializedCompositorSessionSnapshotJSON blob that captures the agenton layer state (includingPydanticAIHistoryLayermessage history) for cross-turn LLM context continuity .
Load or create flow (load_or_create()):
- Looks up the caller object (a
ConversationorAgentConfigDraft) byagent_workspace_binding_id. - If
None(first turn): callsAgentWorkspaceService.create_binding()to allocate a new participant with physical backend resources, then writes the new binding ID back to the caller. - Existing binding: validated via
AgentWorkspaceService.validate_binding_generation()to ensure the generation (config version + home snapshot) matches.
Snapshot save (save_active_snapshot()): after each run completes (successfully, failed, or cancelled), the terminal event's session_snapshot is serialized and persisted to AgentWorkspaceBinding.session_snapshot via AgentWorkspaceService.save_binding_session_snapshot(). A None snapshotβwhen compositor entry failed or layer exit did not completeβleaves the previous snapshot untouched. Only snapshots from runs that reached compositor exit are persisted, enabling resumption from the last valid state even after failures or cancellations.
The AgentAppSessionScope dataclass encodes the full scope key: (tenant_id, app_id, conversation_id, agent_id, agent_config_snapshot_id). When build_draft_id is set, the owner type switches to BUILD_DRAFT instead of CONVERSATION .
Runtime Request Building#
AgentAppRuntimeRequestBuilder.build() assembles the CreateRunRequest sent to the dify-agent backend. Key aspects:
- Model credentials: NOT passed to the agent backend. The
dify-agentruntime calls back to Dify API through/inner/api/agent/llm/invoke, which owns credential resolution and billing/quota metering. System-hosted model calls are charged throughCreditPoolService; custom credentials remain non-billable. The agent invocation ID is reused as the billing request ID for idempotent retries . - Tool layers: plugin tools β
dify.plugin.tools; builtin/API/workflow/MCP tools βdify.core.tools. - Model settings:
agent_soul.model.model_settingsis passed through a whitelist (temperature,top_p,presence_penalty,frequency_penalty,max_tokens,stop_sequences) . - Session snapshot: the
CompositorSessionSnapshotfrom the loaded binding is forwarded assession_snapshotin the run request, enabling thedify-agentservice to restore the previous turn's history layer .- Snapshot compatibility validation: before calling the agent backend, the system validates that the retained session snapshot still matches the current composition via
_validate_session_snapshot_layers(). The method compares the ordered layer names fromrequest.composition.layersagainstsession_snapshot.layers. If the topology has changed (layers added, removed, or reordered), the system raisesAgentSessionSnapshotIncompatibleError(HTTP 409 with code"agent_session_configuration_changed"). Draft rows are updated in place, so their IDs cannot prove that a retained snapshot still belongs to the current composition. Configuration value changes (e.g., prompt edits) WITHOUT topology changes preserve session reuse.
- Snapshot compatibility validation: before calling the agent backend, the system validates that the retained session snapshot still matches the current composition via
backend_binding_ref: the participant identity string fromAgentWorkspaceBinding, required by the agent backend for workspace/resource lookup .- Prompt mentions:
expand_prompt_mentions()resolves slash-menu{{#...#}}tokens before the system prompt reaches the model . - Deferred tool results: set when resuming after an
ask_humanpause . - Multimodal file handling: uploaded files are parsed into access-controlled Dify
Fileobjects viafile_factory.build_from_mapping()inAgentAppGenerator.generate()before reaching the runtime builder. The builder detects whether the selected model supports vision capabilities by callingresolve_model_supports_vision()fromcore.app.llm.model_access, which inspects the credential-bound model schema for theModelFeature.VISIONfeature flag. Image files are sent as native multimodal content through the newDifyUserPromptLayerConfiglayer when vision is supported; the system converts image URLs to Pydantic AIImageUrlcontent and inline Base64 images toBinaryContent. Non-image files and models without vision support fall back to thedify-agent file downloadmechanism (shell-based download instructions appended to the user prompt text).
The builder shares helper functions with the workflow AgentV2 node (e.g., build_knowledge_layer_config, build_shell_layer_config, build_ask_human_layer_config) imported from api/core/workflow/nodes/agent_v2/runtime_request_builder.py.
AgentAppRunner: Stream Consumption and Process Recording#
AgentAppRunner.run() orchestrates a single conversation turn:
- Load/create workspace session β obtain
backend_binding_refand priorsession_snapshot. - Build
AgentAppRuntimeRequest(including deferred tool results if resuming fromask_human). - POST to the agent backend via
create_run()β receiverun_id. _consume_stream(): consume SSE events until a terminal event arrives. The method trackslast_event_idfrom each public event and passessession_scopeandbinding_idto enable session-aware cancellation:- Text deltas (
AgentBackendAgentMessageDeltaInternalEvent): buffered by_TextDeltaDebouncerto avoid per-character SSE frames, then published asQueueAgentMessageEvent. - Process events (
AgentBackendStreamInternalEvent): handled by_AgentProcessRecorderβ writesMessageAgentThoughtDB rows (tool calls, thinking, observations) and firesQueueAgentThoughtEvent. - Stop signal: if
queue_manager.is_stopped(), calls_cancel_run(run_id, after=last_event_id, session_scope, binding_id)to notify the backend, waits for cleanup to complete and persists any returned snapshot, then raisesGenerateTaskStoppedError.
- Text deltas (
- On
AgentBackendRunSucceededInternalEvent: publish final answer β_save_session()persists the snapshot. - On
AgentBackendDeferredToolCallInternalEvent:_pause_for_ask_human()(see HITL section). - On
AgentBackendRunFailedInternalEventorAgentBackendRunCancelledInternalEvent: checks for a non-Nonesession_snapshotand persists it before translatingreasonto anInvokeErrorsubclass via_agent_backend_failure_to_exception().
Error handling in _generate_worker(): AgentSessionSnapshotIncompatibleError is caught and published to the queue manager. This expected topology mismatch (HTTP 409 with code "agent_session_configuration_changed" and message "The Agent configuration changed after this conversation started. Start a new conversation to continue.") is logged at info level rather than triggering an unknown error traceback. The client receives the user-facing instruction as an error event.
The runner emits only four queue event types: QueueAgentMessageEvent, QueueAgentThoughtEvent, QueueLLMChunkEvent, QueueMessageEndEvent. QueueRetrieverResourcesEvent is intentionally absent β retrieval citations are not surfaced in the Agent App SSE stream .
HITL / ask_human Flow#
When the agent calls dify.ask_human, the backend emits AgentBackendDeferredToolCallInternalEvent instead of a success terminal. The runner handles this in _pause_for_ask_human():
- Creates a conversation-owned HITL form (keyed by
message_idasnode_id). - Persists
session_snapshot,pending_form_id, andpending_tool_call_idon theAgentWorkspaceBindingvia_save_session(). - Publishes the agent's question as the turn answer, ending the chat turn from the client's perspective.
Resume: AgentAppGenerator.resume_after_form_submission() is triggered (by a background task) after the user submits the form:
- Re-resolves the agent/soul for the correct draft type using
_resolve_resume_draft(). - Runs
_generate_worker(is_resume=True)β input guards are skipped to avoid short-circuiting the resume with moderation or annotation matching on the replayed query . - The original turn's query is re-sent (not as new user input) to satisfy the agent backend's requirement that layer names in the composition match the suspended snapshot .
- Inside the runner,
_resolve_pending_ask_human()reads the storedpending_form_id, fetches the submitted form result, and builds aDeferredToolResultsPayload. This is forwarded with the session snapshot in theCreateRunRequestto resume the paused backend run.
Key Source Files#
| File | Role |
|---|---|
api/core/app/apps/agent_app/app_generator.py | Entry point: config/session resolution, worker thread, input guards, HITL resume |
api/core/app/apps/agent_app/app_runner.py | Backend delegation, SSE stream consumption, process recording, session save |
api/core/app/apps/agent_app/session_store.py | AgentAppWorkspaceStore: binding load/create/save; AgentAppSessionScope |
api/core/app/apps/agent_app/runtime_request_builder.py | AgentAppRuntimeRequestBuilder: assembles CreateRunRequest |
api/core/app/apps/agent_app/app_config_manager.py | Synthesizes AgentSoulConfig β EasyUI AgentAppConfig for the chat pipeline |
api/core/workflow/nodes/agent_v2/runtime_request_builder.py | Shared helpers: build_knowledge_layer_config, build_shell_layer_config, build_ask_human_layer_config |
api/clients/agent_backend/factory.py | create_agent_backend_run_client(): selects real vs. fake client |
api/configs/extra/agent_backend_config.py | AGENT_BACKEND_BASE_URL, AGENT_BACKEND_API_TOKEN, stream timeout/reconnect settings |
api/models/agent_config_entities.py | AgentSoulConfig and all sub-config Pydantic models |
api/services/agent/workspace_service.py | AgentWorkspaceService: binding allocation, validation, snapshot persistence |