Upstream langgenius/dify state of the touched files before 0ff8f0cd14
This commit is contained in:
commit
318c2c2eda
2 files changed
+936
No files matched your search
@@ -0,0 +1,383 @@
|
|||||||
|
"""Workflow Agent participant persistence keyed by node execution."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from agenton.compositor import CompositorSessionSnapshot
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from core.db.session_factory import session_factory
|
||||||
|
from models.agent import (
|
||||||
|
AgentConfigVersionKind,
|
||||||
|
AgentWorkingResourceStatus,
|
||||||
|
AgentWorkspace,
|
||||||
|
AgentWorkspaceBinding,
|
||||||
|
AgentWorkspaceOwnerType,
|
||||||
|
)
|
||||||
|
from models.workflow import WorkflowNodeExecutionModel
|
||||||
|
from services.agent.workspace_service import (
|
||||||
|
AgentWorkspaceNotFoundError,
|
||||||
|
AgentWorkspaceService,
|
||||||
|
WorkspaceOwnerScope,
|
||||||
|
)
|
||||||
|
|
||||||
|
_CALLER_VISIBILITY_ATTEMPTS = 60
|
||||||
|
_CALLER_VISIBILITY_INTERVAL_SECONDS = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_workflow_agent_workspace_owner_scope(
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
app_id: str,
|
||||||
|
conversation_id: str | None,
|
||||||
|
workflow_run_id: str | None,
|
||||||
|
node_id: str,
|
||||||
|
workflow_agent_binding_id: str,
|
||||||
|
node_execution_id: str | None = None,
|
||||||
|
) -> WorkspaceOwnerScope:
|
||||||
|
"""Choose the Workspace owner for a workflow Agent participant.
|
||||||
|
|
||||||
|
Chatflow runs carry ``conversation_id`` and should persist Agent memory across
|
||||||
|
turns the same way standalone Agent Apps do. Pure workflow runs keep the
|
||||||
|
per-run ``WORKFLOW_RUN`` scope.
|
||||||
|
"""
|
||||||
|
|
||||||
|
owner_scope_key = f"{node_id}:{workflow_agent_binding_id}"
|
||||||
|
if conversation_id:
|
||||||
|
return WorkspaceOwnerScope(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
app_id=app_id,
|
||||||
|
owner_type=AgentWorkspaceOwnerType.CONVERSATION,
|
||||||
|
owner_id=conversation_id,
|
||||||
|
owner_scope_key=owner_scope_key,
|
||||||
|
)
|
||||||
|
return WorkspaceOwnerScope(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
app_id=app_id,
|
||||||
|
owner_type=AgentWorkspaceOwnerType.WORKFLOW_RUN,
|
||||||
|
owner_id=workflow_run_id or node_execution_id or "",
|
||||||
|
owner_scope_key=owner_scope_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class WorkflowAgentSessionScope:
|
||||||
|
tenant_id: str
|
||||||
|
app_id: str
|
||||||
|
workflow_id: str
|
||||||
|
workflow_run_id: str | None
|
||||||
|
node_id: str
|
||||||
|
node_execution_id: str
|
||||||
|
workflow_agent_binding_id: str
|
||||||
|
agent_id: str
|
||||||
|
agent_config_snapshot_id: str
|
||||||
|
conversation_id: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workspace_owner(self) -> WorkspaceOwnerScope:
|
||||||
|
return resolve_workflow_agent_workspace_owner_scope(
|
||||||
|
tenant_id=self.tenant_id,
|
||||||
|
app_id=self.app_id,
|
||||||
|
conversation_id=self.conversation_id,
|
||||||
|
workflow_run_id=self.workflow_run_id,
|
||||||
|
node_id=self.node_id,
|
||||||
|
workflow_agent_binding_id=self.workflow_agent_binding_id,
|
||||||
|
node_execution_id=self.node_execution_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StoredWorkflowAgentSession:
|
||||||
|
scope: WorkflowAgentSessionScope
|
||||||
|
binding_id: str
|
||||||
|
workspace_id: str
|
||||||
|
backend_binding_ref: str
|
||||||
|
session_snapshot: CompositorSessionSnapshot | None
|
||||||
|
pending_form_id: str | None = None
|
||||||
|
pending_tool_call_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowAgentWorkspaceStore:
|
||||||
|
"""Load or create the participant named by a node execution caller row."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_active_participant(
|
||||||
|
*,
|
||||||
|
session: Session,
|
||||||
|
scope: WorkspaceOwnerScope,
|
||||||
|
agent_id: str,
|
||||||
|
) -> AgentWorkspaceBinding | None:
|
||||||
|
"""Look up the participant that pins a Chatflow node's generation."""
|
||||||
|
|
||||||
|
return AgentWorkspaceService.resolve_active_binding_for_scope(
|
||||||
|
session=session,
|
||||||
|
scope=scope,
|
||||||
|
agent_id=agent_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_existing_node_execution_scope(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
app_id: str,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_run_id: str | None,
|
||||||
|
node_id: str,
|
||||||
|
node_execution_id: str,
|
||||||
|
conversation_id: str | None = None,
|
||||||
|
) -> WorkflowAgentSessionScope | None:
|
||||||
|
"""Return the generation pinned by an existing node execution participant."""
|
||||||
|
|
||||||
|
with session_factory.create_session() as session:
|
||||||
|
execution = self._load_execution_by_identity(
|
||||||
|
session=session,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
app_id=app_id,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
workflow_run_id=workflow_run_id,
|
||||||
|
node_id=node_id,
|
||||||
|
node_execution_id=node_execution_id,
|
||||||
|
)
|
||||||
|
binding_id = execution.agent_workspace_binding_id
|
||||||
|
if binding_id is None:
|
||||||
|
return None
|
||||||
|
process_data = execution.process_data_dict
|
||||||
|
if not isinstance(process_data, dict):
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node execution caller identity is invalid")
|
||||||
|
workflow_agent_binding_id = process_data.get("workflow_agent_binding_id")
|
||||||
|
if not isinstance(workflow_agent_binding_id, str):
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node execution caller identity is missing")
|
||||||
|
owner_scope = resolve_workflow_agent_workspace_owner_scope(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
app_id=app_id,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
workflow_run_id=workflow_run_id,
|
||||||
|
node_id=node_id,
|
||||||
|
workflow_agent_binding_id=workflow_agent_binding_id,
|
||||||
|
node_execution_id=node_execution_id,
|
||||||
|
)
|
||||||
|
binding = AgentWorkspaceService.get_active_binding(
|
||||||
|
session=session,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
binding_id=binding_id,
|
||||||
|
expected_owner_scope=owner_scope,
|
||||||
|
)
|
||||||
|
if binding is None or binding.agent_config_version_kind != AgentConfigVersionKind.SNAPSHOT:
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node participant Binding is unavailable")
|
||||||
|
return WorkflowAgentSessionScope(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
app_id=app_id,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
workflow_run_id=workflow_run_id,
|
||||||
|
node_id=node_id,
|
||||||
|
node_execution_id=node_execution_id,
|
||||||
|
workflow_agent_binding_id=workflow_agent_binding_id,
|
||||||
|
agent_id=binding.agent_id,
|
||||||
|
agent_config_snapshot_id=binding.agent_config_version_id,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_or_create_node_execution_session(
|
||||||
|
self, scope: WorkflowAgentSessionScope, *, home_snapshot_id: str | None
|
||||||
|
) -> StoredWorkflowAgentSession:
|
||||||
|
with session_factory.create_session() as session:
|
||||||
|
execution = self._load_execution(session=session, scope=scope)
|
||||||
|
process_data = execution.process_data_dict
|
||||||
|
if process_data is None:
|
||||||
|
process_data = {}
|
||||||
|
if not isinstance(process_data, dict):
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node execution caller identity is invalid")
|
||||||
|
stored_workflow_binding_id = process_data.get("workflow_agent_binding_id")
|
||||||
|
if stored_workflow_binding_id is not None and stored_workflow_binding_id != scope.workflow_agent_binding_id:
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node execution caller identity does not match")
|
||||||
|
|
||||||
|
binding_id = execution.agent_workspace_binding_id
|
||||||
|
if binding_id is None:
|
||||||
|
binding = self._resolve_or_create_binding(
|
||||||
|
session=session,
|
||||||
|
scope=scope,
|
||||||
|
home_snapshot_id=home_snapshot_id,
|
||||||
|
)
|
||||||
|
execution.agent_workspace_binding_id = binding.id
|
||||||
|
execution.process_data = json.dumps(
|
||||||
|
{
|
||||||
|
**process_data,
|
||||||
|
"workflow_agent_binding_id": scope.workflow_agent_binding_id,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
if stored_workflow_binding_id is None:
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node execution caller identity is missing")
|
||||||
|
resolved_binding = AgentWorkspaceService.get_active_binding(
|
||||||
|
session=session,
|
||||||
|
tenant_id=scope.tenant_id,
|
||||||
|
binding_id=binding_id,
|
||||||
|
expected_owner_scope=scope.workspace_owner,
|
||||||
|
)
|
||||||
|
if resolved_binding is None or resolved_binding.agent_id != scope.agent_id:
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node participant Binding is unavailable")
|
||||||
|
binding = resolved_binding
|
||||||
|
AgentWorkspaceService.validate_binding_generation(
|
||||||
|
binding,
|
||||||
|
base_home_snapshot_id=home_snapshot_id,
|
||||||
|
agent_config_version_id=scope.agent_config_snapshot_id,
|
||||||
|
agent_config_version_kind=AgentConfigVersionKind.SNAPSHOT,
|
||||||
|
)
|
||||||
|
return self._stored(session, scope, binding)
|
||||||
|
|
||||||
|
def save_active_snapshot(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
scope: WorkflowAgentSessionScope,
|
||||||
|
binding_id: str,
|
||||||
|
snapshot: CompositorSessionSnapshot | None,
|
||||||
|
pending_form_id: str | None = None,
|
||||||
|
pending_tool_call_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
if snapshot is None:
|
||||||
|
return
|
||||||
|
AgentWorkspaceService.save_binding_session_snapshot(
|
||||||
|
tenant_id=scope.tenant_id,
|
||||||
|
binding_id=binding_id,
|
||||||
|
session_snapshot=snapshot.model_dump_json(),
|
||||||
|
pending_form_id=pending_form_id,
|
||||||
|
pending_tool_call_id=pending_tool_call_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def retire_workflow_run(self, *, tenant_id: str, app_id: str, workflow_run_id: str) -> list[str]:
|
||||||
|
"""Retire active Workspaces, commit, and return active or already-retired IDs for collection."""
|
||||||
|
|
||||||
|
retired: list[str] = []
|
||||||
|
with session_factory.create_session() as session:
|
||||||
|
workspaces = session.scalars(
|
||||||
|
select(AgentWorkspace).where(
|
||||||
|
AgentWorkspace.tenant_id == tenant_id,
|
||||||
|
AgentWorkspace.app_id == app_id,
|
||||||
|
AgentWorkspace.owner_type == AgentWorkspaceOwnerType.WORKFLOW_RUN,
|
||||||
|
AgentWorkspace.owner_id == workflow_run_id,
|
||||||
|
AgentWorkspace.status.in_((AgentWorkingResourceStatus.ACTIVE, AgentWorkingResourceStatus.RETIRED)),
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
for workspace in workspaces:
|
||||||
|
if workspace.status == AgentWorkingResourceStatus.RETIRED:
|
||||||
|
retired.append(workspace.id)
|
||||||
|
continue
|
||||||
|
workspace_id = AgentWorkspaceService.retire_workspace(
|
||||||
|
session=session,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
)
|
||||||
|
if workspace_id is not None:
|
||||||
|
retired.append(workspace_id)
|
||||||
|
session.commit()
|
||||||
|
return retired
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_execution(*, session: Session, scope: WorkflowAgentSessionScope) -> WorkflowNodeExecutionModel:
|
||||||
|
return WorkflowAgentWorkspaceStore._load_execution_by_identity(
|
||||||
|
session=session,
|
||||||
|
tenant_id=scope.tenant_id,
|
||||||
|
app_id=scope.app_id,
|
||||||
|
workflow_id=scope.workflow_id,
|
||||||
|
workflow_run_id=scope.workflow_run_id,
|
||||||
|
node_id=scope.node_id,
|
||||||
|
node_execution_id=scope.node_execution_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_execution_by_identity(
|
||||||
|
*,
|
||||||
|
session: Session,
|
||||||
|
tenant_id: str,
|
||||||
|
app_id: str,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_run_id: str | None,
|
||||||
|
node_id: str,
|
||||||
|
node_execution_id: str,
|
||||||
|
) -> WorkflowNodeExecutionModel:
|
||||||
|
"""Wait briefly for the already-emitted node-start event to persist its caller row."""
|
||||||
|
|
||||||
|
stmt = select(WorkflowNodeExecutionModel).where(
|
||||||
|
WorkflowNodeExecutionModel.id == node_execution_id,
|
||||||
|
WorkflowNodeExecutionModel.tenant_id == tenant_id,
|
||||||
|
WorkflowNodeExecutionModel.app_id == app_id,
|
||||||
|
WorkflowNodeExecutionModel.workflow_id == workflow_id,
|
||||||
|
WorkflowNodeExecutionModel.node_id == node_id,
|
||||||
|
WorkflowNodeExecutionModel.workflow_run_id == workflow_run_id,
|
||||||
|
)
|
||||||
|
for attempt in range(_CALLER_VISIBILITY_ATTEMPTS):
|
||||||
|
execution = session.scalar(stmt)
|
||||||
|
if execution is not None:
|
||||||
|
return execution
|
||||||
|
if attempt < _CALLER_VISIBILITY_ATTEMPTS - 1:
|
||||||
|
time.sleep(_CALLER_VISIBILITY_INTERVAL_SECONDS)
|
||||||
|
|
||||||
|
raise AgentWorkspaceNotFoundError("Workflow node execution caller is unavailable")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_or_create_binding(
|
||||||
|
*,
|
||||||
|
session: Session,
|
||||||
|
scope: WorkflowAgentSessionScope,
|
||||||
|
home_snapshot_id: str | None,
|
||||||
|
) -> AgentWorkspaceBinding:
|
||||||
|
"""Reuse a conversation-scoped participant or allocate a new one."""
|
||||||
|
|
||||||
|
if scope.conversation_id is not None:
|
||||||
|
existing_binding = WorkflowAgentWorkspaceStore.load_active_participant(
|
||||||
|
session=session,
|
||||||
|
scope=scope.workspace_owner,
|
||||||
|
agent_id=scope.agent_id,
|
||||||
|
)
|
||||||
|
if existing_binding is not None:
|
||||||
|
AgentWorkspaceService.validate_binding_generation(
|
||||||
|
existing_binding,
|
||||||
|
base_home_snapshot_id=home_snapshot_id,
|
||||||
|
agent_config_version_id=scope.agent_config_snapshot_id,
|
||||||
|
agent_config_version_kind=AgentConfigVersionKind.SNAPSHOT,
|
||||||
|
)
|
||||||
|
return existing_binding
|
||||||
|
|
||||||
|
return AgentWorkspaceService.create_binding(
|
||||||
|
session=session,
|
||||||
|
scope=scope.workspace_owner,
|
||||||
|
agent_id=scope.agent_id,
|
||||||
|
base_home_snapshot_id=home_snapshot_id,
|
||||||
|
agent_config_version_id=scope.agent_config_snapshot_id,
|
||||||
|
agent_config_version_kind=AgentConfigVersionKind.SNAPSHOT,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _stored(
|
||||||
|
session: Session,
|
||||||
|
scope: WorkflowAgentSessionScope,
|
||||||
|
binding: AgentWorkspaceBinding,
|
||||||
|
) -> StoredWorkflowAgentSession:
|
||||||
|
snapshot = (
|
||||||
|
CompositorSessionSnapshot.model_validate_json(binding.session_snapshot)
|
||||||
|
if binding.session_snapshot
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return StoredWorkflowAgentSession(
|
||||||
|
scope=scope,
|
||||||
|
binding_id=binding.id,
|
||||||
|
workspace_id=binding.workspace_id,
|
||||||
|
backend_binding_ref=binding.backend_binding_ref,
|
||||||
|
session_snapshot=snapshot,
|
||||||
|
pending_form_id=binding.pending_form_id,
|
||||||
|
pending_tool_call_id=binding.pending_tool_call_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"StoredWorkflowAgentSession",
|
||||||
|
"WorkflowAgentSessionScope",
|
||||||
|
"WorkflowAgentWorkspaceStore",
|
||||||
|
"resolve_workflow_agent_workspace_owner_scope",
|
||||||
|
]
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
import json
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from contextlib import nullcontext
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from agenton.compositor import CompositorSessionSnapshot
|
||||||
|
from agenton.compositor.schemas import LayerSessionSnapshot
|
||||||
|
from agenton.layers.base import LifecycleState
|
||||||
|
from sqlalchemy import Engine, event, func, select
|
||||||
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
|
|
||||||
|
from core.workflow.nodes.agent_v2.session_store import (
|
||||||
|
WorkflowAgentSessionScope,
|
||||||
|
WorkflowAgentWorkspaceStore,
|
||||||
|
resolve_workflow_agent_workspace_owner_scope,
|
||||||
|
)
|
||||||
|
from graphon.enums import WorkflowNodeExecutionStatus
|
||||||
|
from models.agent import (
|
||||||
|
AgentConfigVersionKind,
|
||||||
|
AgentHomeSnapshot,
|
||||||
|
AgentWorkingResourceStatus,
|
||||||
|
AgentWorkspace,
|
||||||
|
AgentWorkspaceBinding,
|
||||||
|
AgentWorkspaceOwnerType,
|
||||||
|
)
|
||||||
|
from models.enums import CreatorUserRole
|
||||||
|
from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom
|
||||||
|
from services.agent.workspace_service import AgentWorkspaceNotFoundError, AgentWorkspaceService
|
||||||
|
|
||||||
|
|
||||||
|
def _scope() -> WorkflowAgentSessionScope:
|
||||||
|
return WorkflowAgentSessionScope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_id="workflow-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
node_id="node-1",
|
||||||
|
node_execution_id="execution-1",
|
||||||
|
workflow_agent_binding_id="workflow-binding-1",
|
||||||
|
agent_id="agent-1",
|
||||||
|
agent_config_snapshot_id="config-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execution_row(
|
||||||
|
*,
|
||||||
|
execution_id: str = "execution-1",
|
||||||
|
workflow_run_id: str = "run-1",
|
||||||
|
binding_id: str | None = None,
|
||||||
|
process_data: dict[str, object] | None = None,
|
||||||
|
) -> WorkflowNodeExecutionModel:
|
||||||
|
return WorkflowNodeExecutionModel(
|
||||||
|
id=execution_id,
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_id="workflow-1",
|
||||||
|
triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
|
||||||
|
workflow_run_id=workflow_run_id,
|
||||||
|
index=1,
|
||||||
|
predecessor_node_id=None,
|
||||||
|
node_execution_id="node-execution-1",
|
||||||
|
node_id="node-1",
|
||||||
|
node_type="agent",
|
||||||
|
title="Agent",
|
||||||
|
agent_workspace_binding_id=binding_id,
|
||||||
|
inputs="{}",
|
||||||
|
process_data=json.dumps(process_data) if process_data is not None else None,
|
||||||
|
outputs=None,
|
||||||
|
status=WorkflowNodeExecutionStatus.RUNNING,
|
||||||
|
error=None,
|
||||||
|
elapsed_time=0,
|
||||||
|
execution_metadata=None,
|
||||||
|
created_by_role=CreatorUserRole.ACCOUNT,
|
||||||
|
created_by="user-1",
|
||||||
|
finished_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _workspace_row(
|
||||||
|
*,
|
||||||
|
workspace_id: str = "workspace-1",
|
||||||
|
tenant_id: str = "tenant-1",
|
||||||
|
app_id: str = "app-1",
|
||||||
|
owner_scope_key: str = "node-1:workflow-binding-1",
|
||||||
|
status: AgentWorkingResourceStatus = AgentWorkingResourceStatus.ACTIVE,
|
||||||
|
) -> AgentWorkspace:
|
||||||
|
return AgentWorkspace(
|
||||||
|
id=workspace_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
app_id=app_id,
|
||||||
|
owner_type=AgentWorkspaceOwnerType.WORKFLOW_RUN,
|
||||||
|
owner_id="run-1",
|
||||||
|
owner_scope_key=owner_scope_key,
|
||||||
|
backend_workspace_ref=f"{workspace_id}-ref",
|
||||||
|
status=status,
|
||||||
|
active_guard=1 if status is AgentWorkingResourceStatus.ACTIVE else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _binding_row(
|
||||||
|
*,
|
||||||
|
binding_id: str = "binding-1",
|
||||||
|
workspace_id: str = "workspace-1",
|
||||||
|
status: AgentWorkingResourceStatus = AgentWorkingResourceStatus.ACTIVE,
|
||||||
|
base_home_snapshot_id: str | None = "home-1",
|
||||||
|
) -> AgentWorkspaceBinding:
|
||||||
|
return AgentWorkspaceBinding(
|
||||||
|
id=binding_id,
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
agent_id="agent-1",
|
||||||
|
base_home_snapshot_id=base_home_snapshot_id,
|
||||||
|
agent_config_version_id="config-1",
|
||||||
|
agent_config_version_kind=AgentConfigVersionKind.SNAPSHOT,
|
||||||
|
backend_binding_ref="backend-binding-1" if binding_id == "binding-1" else f"{binding_id}-ref",
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _home_snapshot() -> AgentHomeSnapshot:
|
||||||
|
return AgentHomeSnapshot(
|
||||||
|
id="home-1",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
agent_id="agent-1",
|
||||||
|
snapshot_ref="home-ref",
|
||||||
|
status=AgentWorkingResourceStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_backend_client(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
|
||||||
|
client = MagicMock()
|
||||||
|
client.create_execution_binding_sync.return_value = SimpleNamespace(
|
||||||
|
binding_ref="backend-binding-1",
|
||||||
|
workspace_ref="workspace-1-ref",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(AgentWorkspaceService, "_client", lambda: nullcontext(client))
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def executed_statements(sqlite_engine: Engine) -> Iterator[list[str]]:
|
||||||
|
statements: list[str] = []
|
||||||
|
|
||||||
|
def record_statement(_connection, _cursor, statement, _parameters, _context, _executemany) -> None:
|
||||||
|
statements.append(statement)
|
||||||
|
|
||||||
|
event.listen(sqlite_engine, "before_cursor_execute", record_statement)
|
||||||
|
try:
|
||||||
|
yield statements
|
||||||
|
finally:
|
||||||
|
event.remove(sqlite_engine, "before_cursor_execute", record_statement)
|
||||||
|
|
||||||
|
|
||||||
|
def _execution_selects(statements: list[str]) -> list[str]:
|
||||||
|
return [statement for statement in statements if "FROM workflow_node_executions" in statement]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scope_uses_node_and_workflow_binding_as_workspace_subscope() -> None:
|
||||||
|
owner = _scope().workspace_owner
|
||||||
|
assert owner.owner_type is AgentWorkspaceOwnerType.WORKFLOW_RUN
|
||||||
|
assert owner.owner_id == "run-1"
|
||||||
|
assert owner.owner_scope_key == "node-1:workflow-binding-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_workspace_owner_scope_uses_conversation_for_chatflow() -> None:
|
||||||
|
owner = resolve_workflow_agent_workspace_owner_scope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
conversation_id="conversation-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
node_id="node-1",
|
||||||
|
workflow_agent_binding_id="workflow-binding-1",
|
||||||
|
)
|
||||||
|
assert owner.owner_type is AgentWorkspaceOwnerType.CONVERSATION
|
||||||
|
assert owner.owner_id == "conversation-1"
|
||||||
|
assert owner.owner_scope_key == "node-1:workflow-binding-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_workspace_owner_scope_keeps_workflow_run_for_pure_workflow() -> None:
|
||||||
|
owner = resolve_workflow_agent_workspace_owner_scope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
conversation_id=None,
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
node_id="node-1",
|
||||||
|
workflow_agent_binding_id="workflow-binding-1",
|
||||||
|
node_execution_id="execution-1",
|
||||||
|
)
|
||||||
|
assert owner.owner_type is AgentWorkspaceOwnerType.WORKFLOW_RUN
|
||||||
|
assert owner.owner_id == "run-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_conversation_scope_reuses_existing_binding_on_later_turn(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
sqlite_session: Session,
|
||||||
|
) -> None:
|
||||||
|
prior_snapshot = CompositorSessionSnapshot(
|
||||||
|
layers=[
|
||||||
|
LayerSessionSnapshot(
|
||||||
|
name="history",
|
||||||
|
lifecycle_state=LifecycleState.SUSPENDED,
|
||||||
|
runtime_state={"turn": 1},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
workspace = AgentWorkspace(
|
||||||
|
id="workspace-conversation",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
owner_type=AgentWorkspaceOwnerType.CONVERSATION,
|
||||||
|
owner_id="conversation-1",
|
||||||
|
owner_scope_key="node-1:workflow-binding-1",
|
||||||
|
backend_workspace_ref="workspace-1-ref",
|
||||||
|
status=AgentWorkingResourceStatus.ACTIVE,
|
||||||
|
active_guard=1,
|
||||||
|
)
|
||||||
|
prior_binding = AgentWorkspaceBinding(
|
||||||
|
id="binding-prior",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
agent_id="agent-1",
|
||||||
|
base_home_snapshot_id="home-1",
|
||||||
|
agent_config_version_id="config-1",
|
||||||
|
agent_config_version_kind=AgentConfigVersionKind.SNAPSHOT,
|
||||||
|
backend_binding_ref="backend-binding-prior",
|
||||||
|
status=AgentWorkingResourceStatus.ACTIVE,
|
||||||
|
session_snapshot=prior_snapshot.model_dump_json(),
|
||||||
|
)
|
||||||
|
execution = _execution_row(
|
||||||
|
execution_id="execution-2",
|
||||||
|
workflow_run_id="run-2",
|
||||||
|
process_data={"workflow_agent_binding_id": "workflow-binding-1"},
|
||||||
|
)
|
||||||
|
sqlite_session.add_all([_home_snapshot(), workspace, prior_binding, execution])
|
||||||
|
sqlite_session.commit()
|
||||||
|
client = _install_backend_client(monkeypatch)
|
||||||
|
|
||||||
|
scope = WorkflowAgentSessionScope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_id="workflow-1",
|
||||||
|
workflow_run_id="run-2",
|
||||||
|
node_id="node-1",
|
||||||
|
node_execution_id="execution-2",
|
||||||
|
workflow_agent_binding_id="workflow-binding-1",
|
||||||
|
agent_id="agent-1",
|
||||||
|
agent_config_snapshot_id="config-1",
|
||||||
|
conversation_id="conversation-1",
|
||||||
|
)
|
||||||
|
stored = WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(scope, home_snapshot_id="home-1")
|
||||||
|
|
||||||
|
assert stored.workspace_id == workspace.id
|
||||||
|
assert stored.binding_id == prior_binding.id
|
||||||
|
assert stored.session_snapshot == prior_snapshot
|
||||||
|
client.create_execution_binding_sync.assert_not_called()
|
||||||
|
assert sqlite_session.scalar(select(func.count()).select_from(AgentWorkspaceBinding)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_existing_scope_reads_the_generation_from_the_persisted_binding(sqlite_session: Session) -> None:
|
||||||
|
sqlite_session.add_all(
|
||||||
|
[
|
||||||
|
_execution_row(
|
||||||
|
binding_id="binding-1",
|
||||||
|
process_data={"workflow_agent_binding_id": "workflow-binding-1"},
|
||||||
|
),
|
||||||
|
_workspace_row(),
|
||||||
|
_binding_row(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
scope = WorkflowAgentWorkspaceStore().load_existing_node_execution_scope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_id="workflow-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
node_id="node-1",
|
||||||
|
node_execution_id="execution-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert scope is not None
|
||||||
|
assert scope.workflow_agent_binding_id == "workflow-binding-1"
|
||||||
|
assert scope.agent_id == "agent-1"
|
||||||
|
assert scope.agent_config_snapshot_id == "config-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_existing_scope_rejects_unavailable_persisted_binding(sqlite_session: Session) -> None:
|
||||||
|
sqlite_session.add(
|
||||||
|
_execution_row(
|
||||||
|
binding_id="binding-missing",
|
||||||
|
process_data={"workflow_agent_binding_id": "workflow-binding-1"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
with pytest.raises(AgentWorkspaceNotFoundError, match="participant Binding is unavailable"):
|
||||||
|
WorkflowAgentWorkspaceStore().load_existing_node_execution_scope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_id="workflow-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
node_id="node-1",
|
||||||
|
node_execution_id="execution-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("home_snapshot_id", ["home-1", None])
|
||||||
|
def test_load_or_create_persists_binding_on_node_execution(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
sqlite_session: Session,
|
||||||
|
home_snapshot_id: str | None,
|
||||||
|
) -> None:
|
||||||
|
execution = _execution_row(process_data={"existing": "value"})
|
||||||
|
rows: list[object] = [execution]
|
||||||
|
if home_snapshot_id is not None:
|
||||||
|
rows.append(_home_snapshot())
|
||||||
|
sqlite_session.add_all(rows)
|
||||||
|
sqlite_session.commit()
|
||||||
|
_install_backend_client(monkeypatch)
|
||||||
|
|
||||||
|
stored = WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(
|
||||||
|
_scope(), home_snapshot_id=home_snapshot_id
|
||||||
|
)
|
||||||
|
|
||||||
|
sqlite_session.expire_all()
|
||||||
|
persisted_execution = sqlite_session.get(WorkflowNodeExecutionModel, execution.id)
|
||||||
|
assert persisted_execution is not None
|
||||||
|
assert stored.workspace_id
|
||||||
|
assert stored.backend_binding_ref == "backend-binding-1"
|
||||||
|
assert persisted_execution.agent_workspace_binding_id == stored.binding_id
|
||||||
|
assert persisted_execution.process_data_dict == {
|
||||||
|
"existing": "value",
|
||||||
|
"workflow_agent_binding_id": "workflow-binding-1",
|
||||||
|
}
|
||||||
|
assert "agent_workspace_binding_id" not in persisted_execution.process_data_dict
|
||||||
|
persisted_binding = sqlite_session.get(AgentWorkspaceBinding, stored.binding_id)
|
||||||
|
assert persisted_binding is not None
|
||||||
|
assert persisted_binding.base_home_snapshot_id == home_snapshot_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_existing_pointer_rejects_missing_workflow_identity(sqlite_session: Session) -> None:
|
||||||
|
execution = _execution_row(binding_id="binding-1", process_data={"existing": "value"})
|
||||||
|
sqlite_session.add(execution)
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
with pytest.raises(AgentWorkspaceNotFoundError, match="caller identity is missing"):
|
||||||
|
WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(_scope(), home_snapshot_id="home-1")
|
||||||
|
|
||||||
|
sqlite_session.expire(execution)
|
||||||
|
assert execution.process_data_dict == {"existing": "value"}
|
||||||
|
assert sqlite_session.scalar(select(func.count()).select_from(AgentWorkspaceBinding)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_existing_pointer_reuses_matching_workflow_identity(sqlite_session: Session) -> None:
|
||||||
|
original_process_data = {
|
||||||
|
"existing": "value",
|
||||||
|
"workflow_agent_binding_id": "workflow-binding-1",
|
||||||
|
}
|
||||||
|
execution = _execution_row(binding_id="binding-1", process_data=original_process_data)
|
||||||
|
sqlite_session.add_all([execution, _workspace_row(), _binding_row()])
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
stored = WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(_scope(), home_snapshot_id="home-1")
|
||||||
|
|
||||||
|
sqlite_session.expire(execution)
|
||||||
|
assert stored.binding_id == "binding-1"
|
||||||
|
assert execution.process_data_dict == original_process_data
|
||||||
|
assert "agent_workspace_binding_id" not in execution.process_data_dict
|
||||||
|
assert sqlite_session.scalar(select(func.count()).select_from(AgentWorkspaceBinding)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_existing_pointer_rejects_conflicting_workflow_identity(sqlite_session: Session) -> None:
|
||||||
|
execution = _execution_row(
|
||||||
|
binding_id="binding-1",
|
||||||
|
process_data={"workflow_agent_binding_id": "workflow-binding-other"},
|
||||||
|
)
|
||||||
|
sqlite_session.add(execution)
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
with pytest.raises(AgentWorkspaceNotFoundError, match="caller identity does not match"):
|
||||||
|
WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(_scope(), home_snapshot_id="home-1")
|
||||||
|
|
||||||
|
assert sqlite_session.scalar(select(func.count()).select_from(AgentWorkspaceBinding)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_or_create_fails_before_binding_create_when_caller_row_is_missing(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
sqlite_session: Session,
|
||||||
|
executed_statements: list[str],
|
||||||
|
) -> None:
|
||||||
|
sleep = MagicMock()
|
||||||
|
monkeypatch.setattr("core.workflow.nodes.agent_v2.session_store.time.sleep", sleep)
|
||||||
|
|
||||||
|
with pytest.raises(AgentWorkspaceNotFoundError, match="Workflow node execution caller is unavailable"):
|
||||||
|
WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(_scope(), home_snapshot_id="home-1")
|
||||||
|
|
||||||
|
assert len(_execution_selects(executed_statements)) == 60
|
||||||
|
assert sleep.call_count == 59
|
||||||
|
assert sqlite_session.scalar(select(func.count()).select_from(AgentWorkspaceBinding)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_existing_scope_waits_for_caller_row_to_become_visible(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
sqlite_session_factory: sessionmaker[Session],
|
||||||
|
executed_statements: list[str],
|
||||||
|
) -> None:
|
||||||
|
sleep = MagicMock()
|
||||||
|
|
||||||
|
def make_caller_visible(_seconds: float) -> None:
|
||||||
|
if sleep.call_count == 2:
|
||||||
|
with sqlite_session_factory() as observer:
|
||||||
|
observer.add(_execution_row())
|
||||||
|
observer.commit()
|
||||||
|
|
||||||
|
sleep.side_effect = make_caller_visible
|
||||||
|
monkeypatch.setattr("core.workflow.nodes.agent_v2.session_store.time.sleep", sleep)
|
||||||
|
|
||||||
|
scope = WorkflowAgentWorkspaceStore().load_existing_node_execution_scope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_id="workflow-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
node_id="node-1",
|
||||||
|
node_execution_id="execution-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert scope is None
|
||||||
|
assert len(_execution_selects(executed_statements)) == 3
|
||||||
|
assert sleep.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_snapshot_targets_binding(sqlite_session: Session) -> None:
|
||||||
|
binding = _binding_row()
|
||||||
|
sqlite_session.add_all([_workspace_row(), binding])
|
||||||
|
sqlite_session.commit()
|
||||||
|
snapshot = CompositorSessionSnapshot(layers=[])
|
||||||
|
|
||||||
|
WorkflowAgentWorkspaceStore().save_active_snapshot(
|
||||||
|
scope=_scope(),
|
||||||
|
binding_id=binding.id,
|
||||||
|
snapshot=snapshot,
|
||||||
|
)
|
||||||
|
|
||||||
|
sqlite_session.expire(binding)
|
||||||
|
assert binding.session_snapshot == snapshot.model_dump_json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_retire_workflow_run_only_retires_matching_tenant_and_app(sqlite_session: Session) -> None:
|
||||||
|
matching = _workspace_row()
|
||||||
|
other_tenant = _workspace_row(workspace_id="workspace-other-tenant", tenant_id="tenant-2")
|
||||||
|
other_app = _workspace_row(
|
||||||
|
workspace_id="workspace-other-app",
|
||||||
|
app_id="app-2",
|
||||||
|
owner_scope_key="node-2:workflow-binding-2",
|
||||||
|
)
|
||||||
|
sqlite_session.add_all([matching, other_tenant, other_app])
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
workspace_ids = WorkflowAgentWorkspaceStore().retire_workflow_run(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
sqlite_session.expire_all()
|
||||||
|
assert matching.status is AgentWorkingResourceStatus.RETIRED
|
||||||
|
assert other_tenant.status is AgentWorkingResourceStatus.ACTIVE
|
||||||
|
assert other_app.status is AgentWorkingResourceStatus.ACTIVE
|
||||||
|
assert workspace_ids == [matching.id]
|
||||||
|
|
||||||
|
|
||||||
|
def test_retire_workflow_run_transitions_active_workspace(sqlite_session: Session) -> None:
|
||||||
|
workspace = _workspace_row()
|
||||||
|
binding = _binding_row()
|
||||||
|
sqlite_session.add_all([workspace, binding])
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
workspace_ids = WorkflowAgentWorkspaceStore().retire_workflow_run(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
sqlite_session.expire_all()
|
||||||
|
assert workspace.status is AgentWorkingResourceStatus.RETIRED
|
||||||
|
assert binding.status is AgentWorkingResourceStatus.RETIRED
|
||||||
|
assert workspace_ids == [workspace.id]
|
||||||
|
|
||||||
|
|
||||||
|
def test_retire_workflow_run_returns_existing_retired_workspace(sqlite_session: Session) -> None:
|
||||||
|
workspace = _workspace_row(status=AgentWorkingResourceStatus.RETIRED)
|
||||||
|
sqlite_session.add(workspace)
|
||||||
|
sqlite_session.commit()
|
||||||
|
|
||||||
|
workspace_ids = WorkflowAgentWorkspaceStore().retire_workflow_run(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_run_id="run-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
sqlite_session.expire(workspace)
|
||||||
|
assert workspace.status is AgentWorkingResourceStatus.RETIRED
|
||||||
|
assert workspace_ids == [workspace.id]
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_scope_creates_binding_without_existing_workspace_ref(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
sqlite_session: Session,
|
||||||
|
) -> None:
|
||||||
|
execution = _execution_row(process_data={"workflow_agent_binding_id": "workflow-binding-1"})
|
||||||
|
sqlite_session.add_all([_home_snapshot(), execution])
|
||||||
|
sqlite_session.commit()
|
||||||
|
client = _install_backend_client(monkeypatch)
|
||||||
|
|
||||||
|
WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(_scope(), home_snapshot_id="home-1")
|
||||||
|
|
||||||
|
request = client.create_execution_binding_sync.call_args.args[0]
|
||||||
|
assert request.existing_workspace_ref is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_conversation_scope_first_turn_creates_binding_without_existing_workspace_ref(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
sqlite_session: Session,
|
||||||
|
) -> None:
|
||||||
|
execution = _execution_row(
|
||||||
|
execution_id="execution-2",
|
||||||
|
workflow_run_id="run-2",
|
||||||
|
process_data={"workflow_agent_binding_id": "workflow-binding-1"},
|
||||||
|
)
|
||||||
|
sqlite_session.add_all([_home_snapshot(), execution])
|
||||||
|
sqlite_session.commit()
|
||||||
|
client = _install_backend_client(monkeypatch)
|
||||||
|
|
||||||
|
scope = WorkflowAgentSessionScope(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
app_id="app-1",
|
||||||
|
workflow_id="workflow-1",
|
||||||
|
workflow_run_id="run-2",
|
||||||
|
node_id="node-1",
|
||||||
|
node_execution_id="execution-2",
|
||||||
|
workflow_agent_binding_id="workflow-binding-1",
|
||||||
|
agent_id="agent-1",
|
||||||
|
agent_config_snapshot_id="config-1",
|
||||||
|
conversation_id="conversation-1",
|
||||||
|
)
|
||||||
|
WorkflowAgentWorkspaceStore().load_or_create_node_execution_session(scope, home_snapshot_id="home-1")
|
||||||
|
|
||||||
|
request = client.create_execution_binding_sync.call_args.args[0]
|
||||||
|
assert request.existing_workspace_ref is None
|
||||||
Reference in new issue
Block a user