VirtualWorkflowSynthesizer._build_features() now extracts ALL legacy
app features from AppModelConfig into the synthesized workflow.features:
- opening_statement + suggested_questions
- sensitive_word_avoidance (keywords/API moderation)
- more_like_this
- speech_to_text / text_to_speech
- retriever_resource
Previously workflow.features was hardcoded to "{}", losing all these
features during transparent upgrade. Now AdvancedChatAppRunner's
moderation, opening text, and other feature layers work correctly
for transparently upgraded old apps.
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
- workflow_execute_task: add AppMode.CHAT/AGENT_CHAT/COMPLETION to the
AdvancedChatAppGenerator routing branch so transparently upgraded old
apps can execute through the workflow engine.
- app_generate_service: use app_model.mode (not hardcoded AppMode.AGENT)
for SSE event subscription channel, ensuring the subscriber and
Celery publisher use the same Redis channel key.
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