From 367c095e5c3a6027e915276006942a9864c8f052 Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 17:18:05 +0800 Subject: [PATCH] fix: resume stopped agent-compose sandboxes --- plugins/agent_compose_strategy/client/agent_compose.py | 9 +++++++++ .../strategies/dynamic_workflow.py | 8 ++++---- .../tests/test_agent_compose_client.py | 7 ++++--- plugins/agent_compose_workflow/client/agent_compose.py | 9 +++++++++ .../agent_compose_workflow/docs/architecture.zh-CN.md | 6 +++--- .../tests/test_agent_compose_client.py | 2 +- plugins/agent_compose_workflow/tools/run_agent.py | 8 ++++---- 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/plugins/agent_compose_strategy/client/agent_compose.py b/plugins/agent_compose_strategy/client/agent_compose.py index 7c2a0ac..4bee9af 100644 --- a/plugins/agent_compose_strategy/client/agent_compose.py +++ b/plugins/agent_compose_strategy/client/agent_compose.py @@ -325,6 +325,15 @@ def cleanup_policy_keeps_sandbox(value: str) -> bool: return cleanup_policy_to_proto(value) == "RUN_SANDBOX_CLEANUP_POLICY_KEEP_RUNNING" +def cleanup_policy_reuses_sandbox(value: str) -> bool: + """Whether a later run may reuse this run's sandbox. + + A stopped sandbox is still resumable when its id is supplied to + agent-compose. Only remove-on-completion makes the id unusable. + """ + return cleanup_policy_to_proto(value) != "RUN_SANDBOX_CLEANUP_POLICY_REMOVE_ON_COMPLETION" + + def parse_agent_selection(value: str) -> tuple[str, str]: try: payload = json.loads(value) diff --git a/plugins/agent_compose_strategy/strategies/dynamic_workflow.py b/plugins/agent_compose_strategy/strategies/dynamic_workflow.py index daa4b58..426dbed 100644 --- a/plugins/agent_compose_strategy/strategies/dynamic_workflow.py +++ b/plugins/agent_compose_strategy/strategies/dynamic_workflow.py @@ -12,7 +12,7 @@ AgentComposeClient, AgentComposeConfig, AgentComposeError, - cleanup_policy_keeps_sandbox, + cleanup_policy_reuses_sandbox, remember_agent_compose_sandbox_id, resolve_agent_compose_sandbox_id, resolve_agent_reference, @@ -45,9 +45,9 @@ def _invoke(self, parameters: dict[str, Any]) -> Generator[AgentInvokeMessage, N ) ) project_id, agent_name = resolve_agent_reference(client, params.agent) - keep_sandbox = cleanup_policy_keeps_sandbox(params.cleanup_policy) + reuse_sandbox = cleanup_policy_reuses_sandbox(params.cleanup_policy) sandbox_id = "" - if keep_sandbox: + if reuse_sandbox: sandbox_id = resolve_agent_compose_sandbox_id( explicit_sandbox_id=None, dify_session=self.session, @@ -94,7 +94,7 @@ def _invoke(self, parameters: dict[str, Any]) -> Generator[AgentInvokeMessage, N ) raise - if keep_sandbox: + if reuse_sandbox: remember_agent_compose_sandbox_id( explicit_sandbox_id=None, dify_session=self.session, diff --git a/plugins/agent_compose_strategy/tests/test_agent_compose_client.py b/plugins/agent_compose_strategy/tests/test_agent_compose_client.py index 093ce44..8022480 100644 --- a/plugins/agent_compose_strategy/tests/test_agent_compose_client.py +++ b/plugins/agent_compose_strategy/tests/test_agent_compose_client.py @@ -43,7 +43,7 @@ def test_agent_strategy_does_not_expose_manual_sandbox_id() -> None: @responses.activate -def test_agent_strategy_stop_mode_skips_storage_and_emits_distinct_outputs() -> None: +def test_agent_strategy_stop_mode_remembers_sandbox_and_emits_distinct_outputs() -> None: base_url = "http://agent-compose.test" responses.post( base_url + RUN_AGENT_PROCEDURE, @@ -60,7 +60,7 @@ def test_agent_strategy_stop_mode_skips_storage_and_emits_distinct_outputs() -> ) session = Session.empty_session() session.conversation_id = "conversation-1" - session.storage = FailingStorage(ValueError("storage must not be used")) + session.storage = FakeStorage() strategy = DynamicWorkflowAgentStrategy( runtime=AgentRuntime(user_id="user-1"), session=session, @@ -93,6 +93,7 @@ def test_agent_strategy_stop_mode_skips_storage_and_emits_distinct_outputs() -> "warnings": [], } assert b'"sandboxId"' not in responses.calls[0].request.body + assert list(session.storage.values.values()) == [b"sandbox-1"] def test_agent_strategy_accepts_connection_settings() -> None: @@ -684,7 +685,7 @@ def test_run_agent_uses_tool_provider_credentials() -> None: ) session = Session.empty_session() session.conversation_id = "conversation-1" - session.storage = FailingStorage(ValueError("storage must not be used")) + session.storage = FakeStorage() tool = RunAgentTool( runtime=ToolRuntime( credentials={"agent_compose_url": base_url, "agent_compose_token": "token"}, diff --git a/plugins/agent_compose_workflow/client/agent_compose.py b/plugins/agent_compose_workflow/client/agent_compose.py index 7c2a0ac..4bee9af 100644 --- a/plugins/agent_compose_workflow/client/agent_compose.py +++ b/plugins/agent_compose_workflow/client/agent_compose.py @@ -325,6 +325,15 @@ def cleanup_policy_keeps_sandbox(value: str) -> bool: return cleanup_policy_to_proto(value) == "RUN_SANDBOX_CLEANUP_POLICY_KEEP_RUNNING" +def cleanup_policy_reuses_sandbox(value: str) -> bool: + """Whether a later run may reuse this run's sandbox. + + A stopped sandbox is still resumable when its id is supplied to + agent-compose. Only remove-on-completion makes the id unusable. + """ + return cleanup_policy_to_proto(value) != "RUN_SANDBOX_CLEANUP_POLICY_REMOVE_ON_COMPLETION" + + def parse_agent_selection(value: str) -> tuple[str, str]: try: payload = json.loads(value) diff --git a/plugins/agent_compose_workflow/docs/architecture.zh-CN.md b/plugins/agent_compose_workflow/docs/architecture.zh-CN.md index 7143b8d..d87185e 100644 --- a/plugins/agent_compose_workflow/docs/architecture.zh-CN.md +++ b/plugins/agent_compose_workflow/docs/architecture.zh-CN.md @@ -194,13 +194,13 @@ Strategy 目前使用文本填写 Agent 名称,这是因为 Dify 的 Agent Str 沙箱清理策略有三种: -- `stop_on_completion`:任务完成后停止沙箱。当前 Tool 与 Strategy 均默认使用该模式,适合大多数一次性任务。 +- `stop_on_completion`:任务完成后停止沙箱。停止后仍会保存会话到 sandbox id,后续同一 Dify 会话可恢复该 sandbox;适合不希望 runtime 长期运行、但需要多轮对话的任务。 - `keep_running`:任务完成后保持沙箱运行,适合需要跨轮保留文件、进程或运行状态的会话。 - `remove_on_completion`:任务完成后删除沙箱,适合不需要保留环境、希望及时释放资源的任务。 -当使用 `keep_running` 时,插件会根据 Dify 的 conversation id、project id 和 Agent 名称保存 agent-compose 返回的 sandbox id。后续在同一 Dify 会话中再次调用同一 Agent,便可以自动复用原沙箱;同一会话中的不同 Agent 会使用不同沙箱,避免运行环境串用。 +当使用 `keep_running` 或 `stop_on_completion` 时,插件会根据 Dify 的 conversation id、project id 和 Agent 名称保存 agent-compose 返回的 sandbox id。后续在同一 Dify 会话中再次调用同一 Agent,便可以自动复用原沙箱;同一会话中的不同 Agent 会使用不同沙箱,避免运行环境串用。agent-compose 部署需要保留 stopped runtime 的状态,才能恢复 provider 的原生会话。 -非会话型执行如果没有 conversation id,则每次都会创建新沙箱。`stop_on_completion` 和 `remove_on_completion` 也不会读写这份会话存储。插件有意不暴露手工填写 `sandbox_id` 的参数,以减少错误复用运行环境的风险。 +非会话型执行如果没有 conversation id,则每次都会创建新沙箱。`remove_on_completion` 不会读写这份会话存储;插件有意不暴露手工填写 `sandbox_id` 的参数,以减少错误复用运行环境的风险。 简单来说: diff --git a/plugins/agent_compose_workflow/tests/test_agent_compose_client.py b/plugins/agent_compose_workflow/tests/test_agent_compose_client.py index 92d6e0f..c47450f 100644 --- a/plugins/agent_compose_workflow/tests/test_agent_compose_client.py +++ b/plugins/agent_compose_workflow/tests/test_agent_compose_client.py @@ -605,7 +605,7 @@ def test_run_agent_uses_tool_provider_credentials() -> None: ) session = Session.empty_session() session.conversation_id = "conversation-1" - session.storage = FailingStorage(ValueError("storage must not be used")) + session.storage = FakeStorage() tool = RunAgentTool( runtime=ToolRuntime( credentials={"agent_compose_url": base_url, "agent_compose_token": "token"}, diff --git a/plugins/agent_compose_workflow/tools/run_agent.py b/plugins/agent_compose_workflow/tools/run_agent.py index d5bfc86..0e7318b 100644 --- a/plugins/agent_compose_workflow/tools/run_agent.py +++ b/plugins/agent_compose_workflow/tools/run_agent.py @@ -11,7 +11,7 @@ AgentComposeClient, AgentComposeConfig, AgentComposeError, - cleanup_policy_keeps_sandbox, + cleanup_policy_reuses_sandbox, parse_agent_selection, remember_agent_compose_sandbox_id, resolve_agent_compose_sandbox_id, @@ -29,9 +29,9 @@ def _invoke( if not query: raise AgentComposeError("query is required") cleanup_policy = str(tool_parameters.get("cleanup_policy") or "stop_on_completion") - keep_sandbox = cleanup_policy_keeps_sandbox(cleanup_policy) + reuse_sandbox = cleanup_policy_reuses_sandbox(cleanup_policy) sandbox_id = "" - if keep_sandbox: + if reuse_sandbox: sandbox_id = resolve_agent_compose_sandbox_id( explicit_sandbox_id=None, dify_session=self.session, @@ -80,7 +80,7 @@ def _invoke( ) raise - if keep_sandbox: + if reuse_sandbox: remember_agent_compose_sandbox_id( explicit_sandbox_id=None, dify_session=self.session,