- Add model_features property and build_execution_context method to
AgentAppRunner to fix mypy attr-defined errors
- Export WorkflowComment, WorkflowCommentReply, WorkflowCommentMention
from models/__init__.py to fix import errors
- Add NestedNodeGraphRequest, NestedNodeGraphResponse,
NestedNodeParameterSchema to services/workflow/entities.py
- Update test_agent_chat_app_runner: tests for invalid LLM mode and
invalid strategy now reflect unified AgentAppRunner behavior
(no longer raises ValueError for these cases)
Made-with: Cursor
- Fix type errors in dify_graph/nodes/agent/agent_node.py:
- Add missing user_id param to get_agent_tool_runtime call
- Use create_plugin_provider_manager instead of bare ProviderManager()
- Pass provider_manager to ModelManager constructor
- Add access_controller param to file_factory.build_from_mapping
- Fix return type annotation for _fetch_memory
- Fix DB migration chain: update workflow_comments migration to point
to correct parent after sandbox migration removal
- Fix test_app_generate_service: set AGENT_V2_TRANSPARENT_UPGRADE=False
in mock config to prevent transparent upgrade intercepting test flow
- Fix test_app_generator: add scalar method to mock db.session
- Fix test_app_models: add AppMode.AGENT to expected modes set
- Remove unnecessary db.session.close() from agent_chat app_runner
Made-with: Cursor
Root cause: _WORKDIR was hardcoded to "/home/user" which doesn't exist
in AWS AgentCore Code Interpreter environment (actual pwd is
/opt/amazon/genesis1p-tools/var). Every command was prefixed with
"cd /home/user && ..." which failed silently, producing empty stdout.
Fix:
- Default _WORKDIR to "/tmp" (universally available)
- Auto-detect actual working directory via "pwd" during
_construct_environment and override _WORKDIR dynamically
Verified: echo, python3, uname all return correct stdout.
Made-with: Cursor
Full pipeline working: Agent V2 node → Docker container creation →
CLI binary upload (linux/arm64) → dify init (fetch tools from API) →
dify execute (tool callback via CLI API) → result returned.
Fixes:
- Use sandbox.id (not vm.metadata.id) for CLI paths
- Upload CLI binary to container during sandbox creation
- Resolve linux binary separately for Docker containers on macOS
- Save Docker provider config via SandboxProviderService (proper
encryption) instead of raw DB insert
- Add verbose logging for sandbox tool execution path
- Fix NameError: binary not defined
Made-with: Cursor
- Skip Local sandbox provider under gevent worker (subprocess pipes
cause cooperative threading deadlock with Celery's gevent pool).
- Add non-blocking sandbox readiness check before tool execution.
- Add gevent timeout wrapper for sandbox bash session.
- Fix CLI binary resolution: add SANDBOX_DIFY_CLI_ROOT config field.
- Fix ExecutionContext.node_id propagation.
- Fix SkillInitializer to gracefully handle missing skill bundles.
- Update _invoke_tool_in_sandbox to use correct `dify execute` CLI
subcommand format (not `invoke-tool`).
The full sandbox-in-agent pipeline works end-to-end for network-based
providers (Docker, E2B, SSH). Local provider is skipped under gevent
but works in non-gevent contexts.
Made-with: Cursor
Close 3 integration gaps between the ported Sandbox system and Agent V2:
1. Fix _invoke_tool_in_sandbox to use SandboxBashSession context manager
API correctly (keyword args, bash_tool, ToolReference), with graceful
fallback to direct invocation when DifyCli binary is unavailable.
2. Inject sandbox into run_context via _resolve_sandbox_context() in
WorkflowBasedAppRunner — automatically creates a sandbox when a
tenant has an active sandbox provider configured.
3. Register SandboxLayer in both advanced_chat and workflow app runners
for proper sandbox lifecycle cleanup on graph end.
Also: make SkillInitializer non-fatal when no skill bundle exists,
add node_id to ExecutionContext for sandbox session scoping.
Made-with: Cursor
- Auto-resolve parent_message_id when not provided by client,
querying the latest message in the conversation to maintain
the thread chain that extract_thread_messages() relies on.
- Add AppMode.AGENT to TokenBufferMemory mode checks so file
attachments in memory are handled via the workflow branch.
- Add debug logging for memory injection in node_factory and node.
Made-with: Cursor
1. DSL Import fix: change self._session.commit() to self._session.flush()
in app_dsl_service.py _create_or_update_app() to avoid "closed transaction"
error. DSL import now works: export agent app -> import -> new app created.
2. Memory loading attempt: added _load_memory_messages() to AgentV2Node
that loads TokenBufferMemory from conversation history. However, chatflow
engine manages conversations differently from easy-UI (conversation may
not be in DB at query time, or uses ConversationVariablePersistenceLayer
instead of Message table). Memory needs further investigation.
Test results:
- Multi-turn memory: Turn 1 OK, Turn 2 LLM doesn't see history (needs deeper fix)
- Service API with API Key: PASSED (answer="Sixteen" for 8+8)
- DSL Import: PASSED (status=completed, new app created)
- Token aggregation: PASSED (node=49, workflow=49)
Known: memory in multi-turn chatflow needs to use graphon's built-in
memory mechanism (MemoryConfig on node + ConversationVariablePersistenceLayer)
rather than direct DB query.
Made-with: Cursor
1. Fix workflow-level total_tokens=0:
Call graph_runtime_state.add_tokens(usage.total_tokens) in both
_run_without_tools and _run_with_tools paths after node execution.
Previously only graphon's internal ModelInvokeCompletedEvent handler
called add_tokens, which agent-v2 doesn't emit.
2. Fix Turn 2 SSE empty response:
Set PUBSUB_REDIS_CHANNEL_TYPE=streams in .env. Redis Streams
provides durable event delivery (consumers can replay past events),
solving the pub/sub at-most-once timing issue.
3. Skill -> Agent runtime integration:
SandboxBuilder.build() now auto-includes SkillInitializer if not
already present. This ensures sandbox.attrs has the skill bundle
loaded for downstream consumers (tool execution in sandbox).
4. LegacyResponseAdapter:
New module at core/app/apps/common/legacy_response_adapter.py.
Filters workflow-specific SSE events (workflow_started, node_started,
node_finished, workflow_finished) from the stream, passing through
only message/message_end/agent_log/error/ping events that old
clients expect.
46 unit tests pass.
Made-with: Cursor
The EventAdapter was converting every LLMResultChunk from the agent
strategy into StreamChunkEvent. Combined with the answer node's
{{#agent.text#}} variable output, this caused the final answer to
appear twice (e.g., "It is 2026-04-09 04:27:45.It is 2026-04-09 04:27:45.").
Now LLMResultChunk from strategy output is silently consumed (text still
accumulates in AgentResult.text via the strategy). Only AgentLogEvent
(thought/tool_call/round) is forwarded to the pipeline.
Known remaining issues:
- workflow/message level total_tokens=0 (node level is correct at 33)
because pipeline aggregation doesn't include agent-v2 node tokens
- Turn 2 SSE delivery timing with Redis pubsub (celery executes OK)
Made-with: Cursor
FunctionCallStrategy and ReActStrategy were passing user=self.context.user_id
to ModelInstance.invoke_llm() which doesn't accept that parameter.
This caused tool-using agent runs to fail with:
"ModelInstance.invoke_llm() got an unexpected keyword argument 'user'"
Verified: Agent V2 with current_time tool now works end-to-end:
ROUND 1: LLM thought -> CALL current_time -> got time
ROUND 2: LLM generates answer with time info
Made-with: Cursor
1. Remove StreamChunkEvent from AgentV2Node._run_without_tools():
The agent-v2 node was yielding StreamChunkEvent during LLM streaming,
AND the downstream answer node was outputting the same text via
{{#agent.text#}} variable reference, causing "FourFour" duplication.
Now text only flows through outputs.text -> answer node (single path).
2. Map inputs to query for completion app transparent upgrade:
Completion apps send {inputs: {query: "..."}} not {query: "..."}.
VirtualWorkflowSynthesizer route now extracts query from inputs
when the top-level query is missing.
Verified:
- Old chat app: "What is 2+2?" -> "Four" (was "FourFour")
- Old completion app: {inputs: {query: "What is 3+3?"}} -> "3 + 3 = 6" (was failing)
- Old agent-chat app: still works
Made-with: Cursor
Add two feature-flag-controlled upgrade paths that allow existing apps
and LLM nodes to transparently run through the Agent V2 engine without
any database migration:
1. AGENT_V2_TRANSPARENT_UPGRADE (default: off):
When enabled, old apps (chat/completion/agent-chat) bypass legacy
Easy-UI runners. VirtualWorkflowSynthesizer converts AppModelConfig
to an in-memory Workflow (start -> agent-v2 -> answer) at runtime,
then executes via AdvancedChatAppGenerator. Falls back to legacy
path on any synthesis error.
VirtualWorkflowSynthesizer maps:
- model JSON -> ModelConfig
- pre_prompt/chat_prompt_config -> prompt_template
- agent_mode.tools -> ToolMetadata[]
- agent_mode.strategy -> agent_strategy
- dataset_configs -> context
- file_upload -> vision
2. AGENT_V2_REPLACES_LLM (default: off):
When enabled, DifyNodeFactory.create_node() transparently remaps
nodes with type="llm" to type="agent-v2" before class resolution.
Since AgentV2NodeData is a strict superset of LLMNodeData, the
mapping is lossless. With tools=[], Agent V2 behaves identically
to LLM Node.
Both flags default to False for safety. Turn off = instant rollback.
46 existing tests pass. Flask starts successfully.
Made-with: Cursor