This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new e714b1df7ff Add Vertex AI Agent Engine operators (Create, Get, Query, 
Update, Delete) (#68479)
e714b1df7ff is described below

commit e714b1df7ffeeba8a22076384fa4be4d6a36fdfd
Author: Morgan <[email protected]>
AuthorDate: Sat Jul 11 18:44:39 2026 -0300

    Add Vertex AI Agent Engine operators (Create, Get, Query, Update, Delete) 
(#68479)
---
 providers/google/README.rst                        |   1 +
 providers/google/docs/index.rst                    |   1 +
 .../google/docs/operators/cloud/vertex_ai.rst      |  65 +++
 providers/google/provider.yaml                     |   2 +
 providers/google/pyproject.toml                    |   1 +
 .../google/cloud/hooks/vertex_ai/agent_engine.py   | 375 +++++++++++++++++
 .../cloud/operators/vertex_ai/agent_engine.py      | 443 ++++++++++++++++++++
 .../providers/google/cloud/triggers/vertex_ai.py   | 116 +++++
 .../airflow/providers/google/get_provider_info.py  |   2 +
 .../vertex_ai/example_vertex_ai_agent_engine.py    | 280 +++++++++++++
 .../vertex_ai/resources/agent_engine/Dockerfile    |  27 ++
 .../vertex_ai/resources/agent_engine/__init__.py   |  16 +
 .../resources/agent_engine/hello_agent.py          |  55 +++
 .../cloud/hooks/vertex_ai/test_agent_engine.py     | 385 +++++++++++++++++
 .../cloud/operators/vertex_ai/test_agent_engine.py | 465 +++++++++++++++++++++
 .../cloud/triggers/test_vertex_ai_agent_engine.py  | 207 +++++++++
 uv.lock                                            |   2 +
 17 files changed, 2443 insertions(+)

diff --git a/providers/google/README.rst b/providers/google/README.rst
index 3a986ff3462..35bc9fe5f23 100644
--- a/providers/google/README.rst
+++ b/providers/google/README.rst
@@ -76,6 +76,7 @@ PIP package                                 Version required
 ``google-api-python-client``                ``>=2.0.2``
 ``google-auth``                             ``>=2.29.0``
 ``google-auth-httplib2``                    ``>=0.0.1``
+``google-genai``                            ``>=2.8.0``
 ``google-cloud-aiplatform[evaluation]``     ``>=1.155.0``
 ``ray[default]``                            ``>=2.42.0; python_version < 
"3.13"``
 ``ray[default]``                            ``>=2.49.0; python_version >= 
"3.13" and python_version < "3.14"``
diff --git a/providers/google/docs/index.rst b/providers/google/docs/index.rst
index 19201537b5d..e3bec173806 100644
--- a/providers/google/docs/index.rst
+++ b/providers/google/docs/index.rst
@@ -129,6 +129,7 @@ PIP package                                 Version required
 ``google-api-python-client``                ``>=2.0.2``
 ``google-auth``                             ``>=2.29.0``
 ``google-auth-httplib2``                    ``>=0.0.1``
+``google-genai``                            ``>=2.8.0``
 ``google-cloud-aiplatform[evaluation]``     ``>=1.155.0``
 ``ray[default]``                            ``>=2.42.0; python_version < 
"3.13"``
 ``ray[default]``                            ``>=2.49.0; python_version >= 
"3.13" and python_version < "3.14"``
diff --git a/providers/google/docs/operators/cloud/vertex_ai.rst 
b/providers/google/docs/operators/cloud/vertex_ai.rst
index 246788c9e93..f11b8787ef6 100644
--- a/providers/google/docs/operators/cloud/vertex_ai.rst
+++ b/providers/google/docs/operators/cloud/vertex_ai.rst
@@ -26,6 +26,71 @@ With Vertex AI, both AutoML training and custom training are 
available options.
 Whichever option you choose for training, you can save models, deploy models, 
and
 request predictions with Vertex AI.
 
+Managing Agent Engines
+^^^^^^^^^^^^^^^^^^^^^^
+
+The operators below manage `Vertex AI Agent Engine
+<https://docs.cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/overview>`__
 resources.
+
+To create a Vertex AI Agent Engine you can use
+:class:`~airflow.providers.google.cloud.operators.vertex_ai.agent_engine.CreateAgentEngineOperator`.
+
+.. exampleinclude:: 
/../../google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
+    :language: python
+    :dedent: 4
+    :start-after: [START how_to_cloud_vertex_ai_create_agent_engine_operator]
+    :end-before: [END how_to_cloud_vertex_ai_create_agent_engine_operator]
+
+To get an Agent Engine you can use
+:class:`~airflow.providers.google.cloud.operators.vertex_ai.agent_engine.GetAgentEngineOperator`.
+
+.. exampleinclude:: 
/../../google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
+    :language: python
+    :dedent: 4
+    :start-after: [START how_to_cloud_vertex_ai_get_agent_engine_operator]
+    :end-before: [END how_to_cloud_vertex_ai_get_agent_engine_operator]
+
+To run a query job on an Agent Engine you can use
+:class:`~airflow.providers.google.cloud.operators.vertex_ai.agent_engine.RunQueryJobOperator`.
+The operator uses the public ``run_query_job`` SDK method. The ``config`` 
parameter
+can include ``query`` and ``output_gcs_uri``. The SDK writes query input and 
output
+through Google Cloud Storage. By default, the operator waits for the query job 
to
+complete and returns the serialized query job result. Set ``retrieve_result`` 
to
+``True`` in ``check_config`` to return the query job result from Google Cloud 
Storage.
+
+.. exampleinclude:: 
/../../google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
+    :language: python
+    :dedent: 4
+    :start-after: [START how_to_cloud_vertex_ai_run_query_job_operator]
+    :end-before: [END how_to_cloud_vertex_ai_run_query_job_operator]
+
+The same operation can be performed in the deferrable mode.
+
+.. exampleinclude:: 
/../../google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
+    :language: python
+    :dedent: 4
+    :start-after: [START 
how_to_cloud_vertex_ai_run_query_job_operator_deferrable]
+    :end-before: [END how_to_cloud_vertex_ai_run_query_job_operator_deferrable]
+
+To update an Agent Engine you can use
+:class:`~airflow.providers.google.cloud.operators.vertex_ai.agent_engine.UpdateAgentEngineOperator`.
+
+.. exampleinclude:: 
/../../google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
+    :language: python
+    :dedent: 4
+    :start-after: [START how_to_cloud_vertex_ai_update_agent_engine_operator]
+    :end-before: [END how_to_cloud_vertex_ai_update_agent_engine_operator]
+
+To delete an Agent Engine you can use
+:class:`~airflow.providers.google.cloud.operators.vertex_ai.agent_engine.DeleteAgentEngineOperator`.
+By default, the operator waits until the delete operation completes.
+
+.. exampleinclude:: 
/../../google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
+    :language: python
+    :dedent: 4
+    :start-after: [START how_to_cloud_vertex_ai_delete_agent_engine_operator]
+    :end-before: [END how_to_cloud_vertex_ai_delete_agent_engine_operator]
+
 Creating Datasets
 ^^^^^^^^^^^^^^^^^
 
diff --git a/providers/google/provider.yaml b/providers/google/provider.yaml
index 7258f1ddc7e..3dbed0b0485 100644
--- a/providers/google/provider.yaml
+++ b/providers/google/provider.yaml
@@ -616,6 +616,7 @@ operators:
       - airflow.providers.google.cloud.operators.vertex_ai.generative_model
       - airflow.providers.google.cloud.operators.vertex_ai.feature_store
       - airflow.providers.google.cloud.operators.vertex_ai.ray
+      - airflow.providers.google.cloud.operators.vertex_ai.agent_engine
   - integration-name: Google Data Studio
     python-modules:
       - airflow.providers.google.cloud.operators.looker
@@ -902,6 +903,7 @@ hooks:
       - airflow.providers.google.cloud.hooks.vertex_ai.generative_model
       - airflow.providers.google.cloud.hooks.vertex_ai.prediction_service
       - airflow.providers.google.cloud.hooks.vertex_ai.feature_store
+      - airflow.providers.google.cloud.hooks.vertex_ai.agent_engine
       - airflow.providers.google.cloud.hooks.vertex_ai.ray
   - integration-name: Google Data Studio
     python-modules:
diff --git a/providers/google/pyproject.toml b/providers/google/pyproject.toml
index 0bb1067364f..1698491c65f 100644
--- a/providers/google/pyproject.toml
+++ b/providers/google/pyproject.toml
@@ -80,6 +80,7 @@ dependencies = [
     "google-api-python-client>=2.0.2",
     "google-auth>=2.29.0",
     "google-auth-httplib2>=0.0.1",
+    "google-genai>=2.8.0",
     # google-cloud-aiplatform doesn't install ray for python 3.12 (issue: 
https://github.com/googleapis/python-aiplatform/issues/5252).
     # Temporarily lock in ray 2.42.0 which is compatible with python 3.12 
until linked issue is solved.
     # Remove the ray dependency as well as google-cloud-bigquery-storage once 
linked issue is fixed
diff --git 
a/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/agent_engine.py
 
b/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/agent_engine.py
new file mode 100644
index 00000000000..5471572d3ce
--- /dev/null
+++ 
b/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/agent_engine.py
@@ -0,0 +1,375 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""This module contains a Google Cloud Vertex AI Agent Engine hook."""
+
+from __future__ import annotations
+
+import time
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any
+
+import google.auth.transport.requests
+from asgiref.sync import sync_to_async
+from vertexai import Client
+
+from airflow.providers.google.common.hooks.base_google import (
+    PROVIDE_PROJECT_ID,
+    GoogleBaseAsyncHook,
+    GoogleBaseHook,
+)
+
+if TYPE_CHECKING:
+    from vertexai._genai import types
+
+
+VERTEX_AI_AGENT_ENGINE_API_VERSION = "v1beta1"
+VERTEX_AI_AGENT_ENGINE_OPERATION_URL = (
+    
"https://{location}-aiplatform.googleapis.com/{api_version}/{operation_name}";
+)
+DEFAULT_AGENT_ENGINE_OPERATION_REQUEST_TIMEOUT = 60.0
+
+
+def extract_operation_id(operation_name: str) -> str:
+    """Extract the operation ID from a fully qualified operation name."""
+    return operation_name.rstrip("/").split("/")[-1]
+
+
+def serialize_value(value: Any) -> Any:
+    """Recursively convert SDK model objects to JSON-serializable types."""
+    if hasattr(value, "model_dump"):
+        return value.model_dump(mode="json")
+    if isinstance(value, dict):
+        return {key: serialize_value(item) for key, item in value.items()}
+    if isinstance(value, list):
+        return [serialize_value(item) for item in value]
+    if isinstance(value, tuple):
+        return tuple(serialize_value(item) for item in value)
+    return value
+
+
+class AgentEngineHook(GoogleBaseHook):
+    """
+    Hook for Google Cloud Vertex AI Agent Engine APIs.
+
+    Wraps the ``agent_engines`` module of the Vertex AI SDK client:
+    
https://docs.cloud.google.com/python/docs/reference/agentplatform/latest/vertexai._genai.agent_engines.AgentEngines
+    """
+
+    def __init__(
+        self,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(
+            gcp_conn_id=gcp_conn_id,
+            impersonation_chain=impersonation_chain,
+            **kwargs,
+        )
+
+    def get_agent_engine_client(self, project_id: str, location: str):
+        """Return the Vertex AI Agent Engine client."""
+        return Client(
+            project=project_id,
+            location=location,
+            credentials=self.get_credentials(),
+        ).agent_engines
+
+    @staticmethod
+    def build_agent_engine_name(project_id: str, location: str, 
agent_engine_id: str) -> str:
+        """Build a fully qualified Agent Engine resource name."""
+        return 
f"projects/{project_id}/locations/{location}/reasoningEngines/{agent_engine_id}"
+
+    @staticmethod
+    def build_operation_name(project_id: str, location: str, operation_id: 
str) -> str:
+        """Build a fully qualified Agent Engine operation name."""
+        return 
f"projects/{project_id}/locations/{location}/operations/{operation_id}"
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def create_agent_engine(
+        self,
+        location: str,
+        agent: Any | None = None,
+        config: types.AgentEngineConfigOrDict | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.AgentEngine:
+        """
+        Create an Agent Engine.
+
+        :param location: Required. The ID of the Google Cloud location that 
the service belongs to.
+        :param agent: Optional. The agent object to deploy.
+        :param config: Optional. Configuration for the Agent Engine.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        client = self.get_agent_engine_client(project_id=project_id, 
location=location)
+        return client.create(agent=agent, config=config)
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def get_agent_engine(
+        self,
+        location: str,
+        agent_engine_id: str,
+        config: types.GetAgentEngineConfigOrDict | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.AgentEngine:
+        """
+        Get an Agent Engine.
+
+        :param location: Required. The ID of the Google Cloud location that 
the service belongs to.
+        :param agent_engine_id: Required. The Agent Engine ID.
+        :param config: Optional. Configuration for getting the Agent Engine.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        client = self.get_agent_engine_client(project_id=project_id, 
location=location)
+        name = self.build_agent_engine_name(project_id, location, 
agent_engine_id)
+        return client.get(name=name, config=config)
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def run_query_job(
+        self,
+        location: str,
+        agent_engine_id: str,
+        config: types.RunQueryJobAgentEngineConfigOrDict | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.RunQueryJobResult:
+        """
+        Run a query job on an Agent Engine.
+
+        :param location: Required. The ID of the Google Cloud location that 
the service belongs to.
+        :param agent_engine_id: Required. The Agent Engine ID.
+        :param config: Optional. Configuration for the query job (``query``, 
``output_gcs_uri``).
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        client = self.get_agent_engine_client(project_id=project_id, 
location=location)
+        name = self.build_agent_engine_name(project_id, location, 
agent_engine_id)
+        return client.run_query_job(name=name, config=config)
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def check_query_agent_engine_job(
+        self,
+        location: str,
+        operation_id: str,
+        config: types.CheckQueryJobAgentEngineConfigOrDict | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.CheckQueryJobResult:
+        """
+        Check a query job on an Agent Engine.
+
+        :param location: Required. The ID of the Google Cloud location that 
the service belongs to.
+        :param operation_id: Required. The query job operation ID.
+        :param config: Optional. Configuration for checking the query job.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        client = self.get_agent_engine_client(project_id=project_id, 
location=location)
+        operation_name = self.build_operation_name(project_id, location, 
operation_id)
+        return client.check_query_job(name=operation_name, config=config)
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def wait_for_query_agent_engine_job(
+        self,
+        location: str,
+        operation_id: str,
+        config: types.CheckQueryJobAgentEngineConfigOrDict | None = None,
+        poll_interval: float = 30,
+        timeout: float | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.CheckQueryJobResult:
+        """
+        Wait until an Agent Engine query job completes.
+
+        :param location: Required. The ID of the Google Cloud location that 
the service belongs to.
+        :param operation_id: Required. The query job operation ID.
+        :param config: Optional. Configuration for checking the query job.
+        :param poll_interval: Time, in seconds, to wait between checks.
+        :param timeout: Optional timeout, in seconds.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        start_time = time.monotonic()
+        operation_name = self.build_operation_name(project_id, location, 
operation_id)
+        while True:
+            query_job = self.check_query_agent_engine_job(
+                project_id=project_id,
+                location=location,
+                operation_id=operation_id,
+                config=config,
+            )
+            status = getattr(query_job, "status", None)
+            if status == "SUCCESS":
+                return query_job
+            if status == "FAILED":
+                raise RuntimeError(f"Agent Engine query job {operation_name} 
failed.")
+            if status not in (None, "RUNNING"):
+                raise RuntimeError(
+                    f"Agent Engine query job {operation_name} completed with 
unexpected status {status}."
+                )
+            if timeout is not None and time.monotonic() - start_time >= 
timeout:
+                raise TimeoutError(f"Timed out waiting for Agent Engine query 
job {operation_name}")
+            self.log.info("Waiting for Agent Engine query job %s to 
complete.", operation_name)
+            time.sleep(poll_interval)
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def update_agent_engine(
+        self,
+        location: str,
+        agent_engine_id: str,
+        config: types.AgentEngineConfigOrDict,
+        agent: Any | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.AgentEngine:
+        """
+        Update an Agent Engine.
+
+        :param location: Required. The ID of the Google Cloud location that 
the service belongs to.
+        :param agent_engine_id: Required. The Agent Engine ID.
+        :param config: Required. Configuration for the Agent Engine update.
+        :param agent: Optional. The updated agent object to deploy.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        client = self.get_agent_engine_client(project_id=project_id, 
location=location)
+        name = self.build_agent_engine_name(project_id, location, 
agent_engine_id)
+        return client.update(name=name, agent=agent, config=config)
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def delete_agent_engine(
+        self,
+        location: str,
+        agent_engine_id: str,
+        force: bool | None = None,
+        config: types.DeleteAgentEngineConfigOrDict | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.DeleteAgentEngineOperation:
+        """
+        Delete an Agent Engine.
+
+        :param location: Required. The ID of the Google Cloud location that 
the service belongs to.
+        :param agent_engine_id: Required. The Agent Engine ID.
+        :param force: Optional. Whether to forcefully delete child resources. 
Defaults to ``False``
+            when not specified.
+        :param config: Optional. Additional deletion configuration.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        client = self.get_agent_engine_client(project_id=project_id, 
location=location)
+        name = self.build_agent_engine_name(project_id, location, 
agent_engine_id)
+        return client.delete(name=name, force=force, config=config)
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def get_agent_engine_operation(
+        self,
+        location: str,
+        operation_id: str,
+        request_timeout: float | None = 
DEFAULT_AGENT_ENGINE_OPERATION_REQUEST_TIMEOUT,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> dict[str, Any]:
+        """
+        Return a Vertex AI Agent Engine long-running operation.
+
+        :param location: The ID of the Google Cloud location that the service 
belongs to.
+        :param operation_id: The Agent Engine operation ID.
+        :param request_timeout: Optional timeout, in seconds, for the 
operation request.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        operation_name = self.build_operation_name(project_id, location, 
operation_id)
+        url = VERTEX_AI_AGENT_ENGINE_OPERATION_URL.format(
+            location=location,
+            api_version=VERTEX_AI_AGENT_ENGINE_API_VERSION,
+            operation_name=operation_name,
+        )
+        session = 
google.auth.transport.requests.AuthorizedSession(self.get_credentials())
+        response = session.get(url, timeout=request_timeout)
+        response.raise_for_status()
+        return response.json()
+
+    @GoogleBaseHook.fallback_to_default_project_id
+    def wait_for_agent_engine_operation(
+        self,
+        location: str,
+        operation_id: str,
+        poll_interval: float = 30,
+        timeout: float | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> None:
+        """
+        Wait until an Agent Engine operation completes.
+
+        :param location: The ID of the Google Cloud location that the service 
belongs to.
+        :param operation_id: The Agent Engine operation ID.
+        :param poll_interval: Time, in seconds, to wait between checks.
+        :param timeout: Optional timeout, in seconds.
+        :param project_id: Optional. The ID of the Google Cloud project. 
Defaults to the project
+            configured in the connection.
+        """
+        start_time = time.monotonic()
+        operation_name = self.build_operation_name(project_id, location, 
operation_id)
+        while True:
+            operation = self.get_agent_engine_operation(
+                project_id=project_id,
+                location=location,
+                operation_id=operation_id,
+            )
+            if operation.get("done"):
+                if operation.get("error"):
+                    raise RuntimeError(
+                        f"Agent Engine operation {operation_name} failed: 
{operation['error']}"
+                    )
+                return
+            if timeout is not None and time.monotonic() - start_time >= 
timeout:
+                raise TimeoutError(f"Timed out waiting for Agent Engine 
operation {operation_name}")
+            self.log.info("Waiting for Agent Engine operation %s to 
complete.", operation_name)
+            time.sleep(poll_interval)
+
+
+class AgentEngineAsyncHook(GoogleBaseAsyncHook):
+    """Async hook for Google Cloud Vertex AI Agent Engine APIs."""
+
+    sync_hook_class = AgentEngineHook
+
+    def __init__(
+        self,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ):
+        super().__init__(
+            gcp_conn_id=gcp_conn_id,
+            impersonation_chain=impersonation_chain,
+            **kwargs,
+        )
+
+    async def check_query_agent_engine_job(
+        self,
+        location: str,
+        operation_id: str,
+        config: types.CheckQueryJobAgentEngineConfigOrDict | None = None,
+        project_id: str = PROVIDE_PROJECT_ID,
+    ) -> types.CheckQueryJobResult:
+        """Check a query job on an Agent Engine."""
+        sync_hook = await self.get_sync_hook()
+        return await sync_to_async(sync_hook.check_query_agent_engine_job)(
+            project_id=project_id,
+            location=location,
+            operation_id=operation_id,
+            config=config,
+        )
diff --git 
a/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/agent_engine.py
 
b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/agent_engine.py
new file mode 100644
index 00000000000..3b6ea3a833d
--- /dev/null
+++ 
b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/agent_engine.py
@@ -0,0 +1,443 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""This module contains Google Vertex AI Agent Engine operators."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from functools import cached_property
+from typing import TYPE_CHECKING, Any
+
+from airflow.providers.common.compat.sdk import conf
+from airflow.providers.google.cloud.hooks.vertex_ai.agent_engine import (
+    AgentEngineHook,
+    extract_operation_id,
+    serialize_value,
+)
+from airflow.providers.google.cloud.operators.cloud_base import 
GoogleCloudBaseOperator
+from airflow.providers.google.cloud.triggers.vertex_ai import 
AgentEngineQueryJobTrigger
+
+if TYPE_CHECKING:
+    from vertexai._genai import types
+
+    from airflow.providers.common.compat.sdk import Context
+
+
+def _serialize_agent_engine(agent_engine: types.AgentEngine) -> dict[str, Any]:
+    api_resource = getattr(agent_engine, "api_resource", None)
+    if api_resource is not None:
+        return serialize_value(api_resource)
+    return serialize_value(agent_engine)
+
+
+class CreateAgentEngineOperator(GoogleCloudBaseOperator):
+    """
+    Create a Vertex AI Agent Engine.
+
+    :param project_id: Required. The ID of the Google Cloud project that the 
service belongs to.
+    :param location: Required. The ID of the Google Cloud location that the 
service belongs to.
+    :param agent: Optional. The agent object to deploy.
+    :param config: Optional. Configuration for the Agent Engine.
+    :param gcp_conn_id: The connection ID to use connecting to Google Cloud.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term credentials.
+    """
+
+    template_fields = (
+        "project_id",
+        "location",
+        "agent",
+        "config",
+        "gcp_conn_id",
+        "impersonation_chain",
+    )
+
+    def __init__(
+        self,
+        *,
+        project_id: str,
+        location: str,
+        agent: Any | None = None,
+        config: types.AgentEngineConfigOrDict | None = None,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.project_id = project_id
+        self.location = location
+        self.agent = agent
+        self.config = config
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    @cached_property
+    def hook(self) -> AgentEngineHook:
+        return AgentEngineHook(
+            gcp_conn_id=self.gcp_conn_id,
+            impersonation_chain=self.impersonation_chain,
+        )
+
+    def execute(self, context: Context) -> dict[str, Any]:
+        self.log.info("Creating Agent Engine.")
+        agent_engine = self.hook.create_agent_engine(
+            project_id=self.project_id,
+            location=self.location,
+            agent=self.agent,
+            config=self.config,
+        )
+        result = _serialize_agent_engine(agent_engine)
+        self.log.info("Agent Engine was created.")
+        return result
+
+
+class GetAgentEngineOperator(GoogleCloudBaseOperator):
+    """
+    Get a Vertex AI Agent Engine.
+
+    :param project_id: Required. The ID of the Google Cloud project that the 
service belongs to.
+    :param location: Required. The ID of the Google Cloud location that the 
service belongs to.
+    :param agent_engine_id: Required. The Agent Engine ID.
+    :param config: Optional. Configuration for getting the Agent Engine.
+    :param gcp_conn_id: The connection ID to use connecting to Google Cloud.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term credentials.
+    """
+
+    template_fields = (
+        "project_id",
+        "location",
+        "agent_engine_id",
+        "config",
+        "gcp_conn_id",
+        "impersonation_chain",
+    )
+
+    def __init__(
+        self,
+        *,
+        project_id: str,
+        location: str,
+        agent_engine_id: str,
+        config: types.GetAgentEngineConfigOrDict | None = None,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.project_id = project_id
+        self.location = location
+        self.agent_engine_id = agent_engine_id
+        self.config = config
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    @cached_property
+    def hook(self) -> AgentEngineHook:
+        return AgentEngineHook(
+            gcp_conn_id=self.gcp_conn_id,
+            impersonation_chain=self.impersonation_chain,
+        )
+
+    def execute(self, context: Context) -> dict[str, Any]:
+        self.log.info("Getting Agent Engine %s.", self.agent_engine_id)
+        agent_engine = self.hook.get_agent_engine(
+            project_id=self.project_id,
+            location=self.location,
+            agent_engine_id=self.agent_engine_id,
+            config=self.config,
+        )
+        result = _serialize_agent_engine(agent_engine)
+        self.log.info("Agent Engine %s was retrieved.", self.agent_engine_id)
+        return result
+
+
+class RunQueryJobOperator(GoogleCloudBaseOperator):
+    """
+    Run a query job on a Vertex AI Agent Engine.
+
+    :param project_id: Required. The ID of the Google Cloud project that the 
service belongs to.
+    :param location: Required. The ID of the Google Cloud location that the 
service belongs to.
+    :param agent_engine_id: Required. The Agent Engine ID.
+    :param config: Optional. Configuration for the query job (``query``, 
``output_gcs_uri``).
+    :param check_config: Optional. Configuration for checking the query job.
+    :param wait_for_completion: Whether to wait until the query job completes.
+    :param poll_interval: Time, in seconds, to wait between checks.
+    :param timeout: Optional timeout, in seconds.
+    :param gcp_conn_id: The connection ID to use connecting to Google Cloud.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term credentials.
+    :param deferrable: Run operator in the deferrable mode.
+    """
+
+    template_fields = (
+        "project_id",
+        "location",
+        "agent_engine_id",
+        "config",
+        "check_config",
+        "gcp_conn_id",
+        "impersonation_chain",
+    )
+
+    def __init__(
+        self,
+        *,
+        project_id: str,
+        location: str,
+        agent_engine_id: str,
+        config: types.RunQueryJobAgentEngineConfigOrDict | None = None,
+        check_config: types.CheckQueryJobAgentEngineConfigOrDict | None = None,
+        wait_for_completion: bool = True,
+        poll_interval: float = 30,
+        timeout: float | None = None,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        deferrable: bool = conf.getboolean("operators", "default_deferrable", 
fallback=False),
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.project_id = project_id
+        self.location = location
+        self.agent_engine_id = agent_engine_id
+        self.config = config
+        self.check_config = check_config
+        self.wait_for_completion = wait_for_completion
+        self.poll_interval = poll_interval
+        self.timeout = timeout
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+        self.deferrable = deferrable
+
+    @cached_property
+    def hook(self) -> AgentEngineHook:
+        return AgentEngineHook(
+            gcp_conn_id=self.gcp_conn_id,
+            impersonation_chain=self.impersonation_chain,
+        )
+
+    def execute(self, context: Context) -> dict[str, Any]:
+        self.log.info("Running query job on Agent Engine %s.", 
self.agent_engine_id)
+        query_job = self.hook.run_query_job(
+            project_id=self.project_id,
+            location=self.location,
+            agent_engine_id=self.agent_engine_id,
+            config=self.config,
+        )
+        result = serialize_value(query_job)
+        self.log.info("Query job was started on Agent Engine %s.", 
self.agent_engine_id)
+        if not self.wait_for_completion:
+            return result
+
+        operation_name = getattr(query_job, "job_name", None)
+        if not operation_name:
+            raise RuntimeError("Agent Engine query job did not include an 
operation name.")
+        operation_id = extract_operation_id(operation_name)
+
+        if self.deferrable:
+            return self.defer(
+                trigger=AgentEngineQueryJobTrigger(
+                    project_id=self.project_id,
+                    location=self.location,
+                    operation_id=operation_id,
+                    config=self.check_config,
+                    gcp_conn_id=self.gcp_conn_id,
+                    impersonation_chain=self.impersonation_chain,
+                    poll_interval=self.poll_interval,
+                    timeout=self.timeout,
+                ),
+                method_name="execute_complete",
+            )
+
+        completed_job = self.hook.wait_for_query_agent_engine_job(
+            project_id=self.project_id,
+            location=self.location,
+            operation_id=operation_id,
+            config=self.check_config,
+            poll_interval=self.poll_interval,
+            timeout=self.timeout,
+        )
+        self.log.info("Agent Engine query job %s completed.", operation_name)
+        return serialize_value(completed_job)
+
+    def execute_complete(self, context: Context, event: dict[str, Any] | None 
= None) -> dict[str, Any]:
+        if event is None:
+            raise RuntimeError("No event received in trigger callback")
+        if event["status"] == "success":
+            self.log.info("Agent Engine query job completed.")
+            return event["query_job"]
+        if event["status"] == "timeout":
+            raise TimeoutError(event["message"])
+        raise RuntimeError(event["message"])
+
+
+class UpdateAgentEngineOperator(GoogleCloudBaseOperator):
+    """
+    Update a Vertex AI Agent Engine.
+
+    :param project_id: Required. The ID of the Google Cloud project that the 
service belongs to.
+    :param location: Required. The ID of the Google Cloud location that the 
service belongs to.
+    :param agent_engine_id: Required. The Agent Engine ID.
+    :param agent: Optional. The updated agent object to deploy.
+    :param config: Required. Configuration for the Agent Engine update.
+    :param gcp_conn_id: The connection ID to use connecting to Google Cloud.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term credentials.
+    """
+
+    template_fields = (
+        "project_id",
+        "location",
+        "agent_engine_id",
+        "agent",
+        "config",
+        "gcp_conn_id",
+        "impersonation_chain",
+    )
+
+    def __init__(
+        self,
+        *,
+        project_id: str,
+        location: str,
+        agent_engine_id: str,
+        config: types.AgentEngineConfigOrDict,
+        agent: Any | None = None,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.project_id = project_id
+        self.location = location
+        self.agent_engine_id = agent_engine_id
+        self.agent = agent
+        self.config = config
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    @cached_property
+    def hook(self) -> AgentEngineHook:
+        return AgentEngineHook(
+            gcp_conn_id=self.gcp_conn_id,
+            impersonation_chain=self.impersonation_chain,
+        )
+
+    def execute(self, context: Context) -> dict[str, Any]:
+        self.log.info("Updating Agent Engine %s.", self.agent_engine_id)
+        agent_engine = self.hook.update_agent_engine(
+            project_id=self.project_id,
+            location=self.location,
+            agent_engine_id=self.agent_engine_id,
+            agent=self.agent,
+            config=self.config,
+        )
+        result = _serialize_agent_engine(agent_engine)
+        self.log.info("Agent Engine %s was updated.", self.agent_engine_id)
+        return result
+
+
+class DeleteAgentEngineOperator(GoogleCloudBaseOperator):
+    """
+    Delete a Vertex AI Agent Engine.
+
+    :param project_id: Required. The ID of the Google Cloud project that the 
service belongs to.
+    :param location: Required. The ID of the Google Cloud location that the 
service belongs to.
+    :param agent_engine_id: Required. The Agent Engine ID.
+    :param force: Optional. Whether to delete child resources.
+    :param config: Optional. Additional deletion configuration.
+    :param wait_for_completion: Whether to wait until the delete operation 
completes.
+    :param poll_interval: Time, in seconds, to wait between checks.
+    :param timeout: Optional timeout, in seconds.
+    :param gcp_conn_id: The connection ID to use connecting to Google Cloud.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term credentials.
+    """
+
+    template_fields = (
+        "project_id",
+        "location",
+        "agent_engine_id",
+        "force",
+        "config",
+        "gcp_conn_id",
+        "impersonation_chain",
+    )
+
+    def __init__(
+        self,
+        *,
+        project_id: str,
+        location: str,
+        agent_engine_id: str,
+        force: bool | None = None,
+        config: types.DeleteAgentEngineConfigOrDict | None = None,
+        wait_for_completion: bool = True,
+        poll_interval: float = 30,
+        timeout: float | None = None,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.project_id = project_id
+        self.location = location
+        self.agent_engine_id = agent_engine_id
+        self.force = force
+        self.config = config
+        self.wait_for_completion = wait_for_completion
+        self.poll_interval = poll_interval
+        self.timeout = timeout
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    @cached_property
+    def hook(self) -> AgentEngineHook:
+        return AgentEngineHook(
+            gcp_conn_id=self.gcp_conn_id,
+            impersonation_chain=self.impersonation_chain,
+        )
+
+    def execute(self, context: Context) -> dict[str, Any]:
+        self.log.info("Deleting Agent Engine %s.", self.agent_engine_id)
+        operation = self.hook.delete_agent_engine(
+            project_id=self.project_id,
+            location=self.location,
+            agent_engine_id=self.agent_engine_id,
+            force=self.force,
+            config=self.config,
+        )
+        result = serialize_value(operation)
+        if not self.wait_for_completion:
+            return result
+
+        operation_name = getattr(operation, "name", None)
+        if not operation_name:
+            raise RuntimeError("Delete Agent Engine operation did not include 
an operation name.")
+        operation_id = extract_operation_id(operation_name)
+
+        if getattr(operation, "done", False):
+            if operation_error := getattr(operation, "error", None):
+                raise RuntimeError(f"Agent Engine operation {operation_name} 
failed: {operation_error}")
+            self.log.info("Agent Engine %s was deleted.", self.agent_engine_id)
+            return result
+
+        self.hook.wait_for_agent_engine_operation(
+            project_id=self.project_id,
+            location=self.location,
+            operation_id=operation_id,
+            poll_interval=self.poll_interval,
+            timeout=self.timeout,
+        )
+        self.log.info("Agent Engine %s was deleted.", self.agent_engine_id)
+        return result
diff --git 
a/providers/google/src/airflow/providers/google/cloud/triggers/vertex_ai.py 
b/providers/google/src/airflow/providers/google/cloud/triggers/vertex_ai.py
index d39e96734fe..6e7ecff02fc 100644
--- a/providers/google/src/airflow/providers/google/cloud/triggers/vertex_ai.py
+++ b/providers/google/src/airflow/providers/google/cloud/triggers/vertex_ai.py
@@ -16,6 +16,8 @@
 # under the License.
 from __future__ import annotations
 
+import asyncio
+import time
 from collections.abc import AsyncIterator, Sequence
 from functools import cached_property
 from typing import TYPE_CHECKING, Any
@@ -29,6 +31,7 @@ from google.cloud.aiplatform_v1 import (
 )
 
 from airflow.providers.common.compat.sdk import AirflowException
+from airflow.providers.google.cloud.hooks.vertex_ai.agent_engine import 
AgentEngineAsyncHook, serialize_value
 from airflow.providers.google.cloud.hooks.vertex_ai.batch_prediction_job 
import BatchPredictionJobAsyncHook
 from airflow.providers.google.cloud.hooks.vertex_ai.custom_job import 
CustomJobAsyncHook
 from airflow.providers.google.cloud.hooks.vertex_ai.hyperparameter_tuning_job 
import (
@@ -39,6 +42,7 @@ from airflow.triggers.base import BaseTrigger, TriggerEvent
 
 if TYPE_CHECKING:
     from proto import Message
+    from vertexai._genai import types as vertexai_types
 
 
 class BaseVertexAIJobTrigger(BaseTrigger):
@@ -126,6 +130,118 @@ class BaseVertexAIJobTrigger(BaseTrigger):
         return self.job_serializer_class.to_dict(job)
 
 
+class AgentEngineQueryJobTrigger(BaseTrigger):
+    """Trigger that waits until a Vertex AI Agent Engine query job 
completes."""
+
+    def __init__(
+        self,
+        project_id: str,
+        location: str,
+        operation_id: str,
+        config: vertexai_types.CheckQueryJobAgentEngineConfigOrDict | None = 
None,
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        poll_interval: float = 30,
+        timeout: float | None = None,
+    ):
+        super().__init__()
+        self.project_id = project_id
+        self.location = location
+        self.operation_id = operation_id
+        self.config = config
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+        self.poll_interval = poll_interval
+        self.timeout = timeout
+
+    def serialize(self) -> tuple[str, dict[str, Any]]:
+        return (
+            
"airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineQueryJobTrigger",
+            {
+                "project_id": self.project_id,
+                "location": self.location,
+                "operation_id": self.operation_id,
+                "config": serialize_value(self.config),
+                "gcp_conn_id": self.gcp_conn_id,
+                "impersonation_chain": self.impersonation_chain,
+                "poll_interval": self.poll_interval,
+                "timeout": self.timeout,
+            },
+        )
+
+    @cached_property
+    def async_hook(self) -> AgentEngineAsyncHook:
+        return AgentEngineAsyncHook(
+            gcp_conn_id=self.gcp_conn_id,
+            impersonation_chain=self.impersonation_chain,
+        )
+
+    async def run(self) -> AsyncIterator[TriggerEvent]:
+        start_time = time.monotonic()
+        try:
+            while True:
+                query_job = await self.async_hook.check_query_agent_engine_job(
+                    project_id=self.project_id,
+                    location=self.location,
+                    operation_id=self.operation_id,
+                    config=self.config,
+                )
+                status = getattr(query_job, "status", None)
+                serialized_query_job = serialize_value(query_job)
+                if status == "SUCCESS":
+                    yield TriggerEvent(
+                        {
+                            "status": "success",
+                            "message": "Agent Engine query job completed",
+                            "query_job": serialized_query_job,
+                        }
+                    )
+                    return
+                if status == "FAILED":
+                    yield TriggerEvent(
+                        {
+                            "status": "error",
+                            "message": f"Agent Engine query job 
{self.operation_id} failed.",
+                            "query_job": serialized_query_job,
+                        }
+                    )
+                    return
+                if status not in (None, "RUNNING"):
+                    yield TriggerEvent(
+                        {
+                            "status": "error",
+                            "message": (
+                                f"Agent Engine query job {self.operation_id} 
completed with "
+                                f"unexpected status {status}."
+                            ),
+                            "query_job": serialized_query_job,
+                        }
+                    )
+                    return
+
+                if self.timeout is not None and time.monotonic() - start_time 
>= self.timeout:
+                    yield TriggerEvent(
+                        {
+                            "status": "timeout",
+                            "message": f"Timed out waiting for Agent Engine 
query job {self.operation_id}",
+                            "query_job": serialized_query_job,
+                        }
+                    )
+                    return
+
+                self.log.info("Waiting for Agent Engine query job %s to 
complete.", self.operation_id)
+                await asyncio.sleep(self.poll_interval)
+        except Exception as err:
+            self.log.exception("Exception occurred while waiting for Agent 
Engine query job.")
+            yield TriggerEvent(
+                {
+                    "status": "error",
+                    "message": f"Failed while polling Agent Engine query job: 
{err}",
+                    "query_job": {"operation_id": self.operation_id},
+                }
+            )
+
+
 class CreateHyperparameterTuningJobTrigger(BaseVertexAIJobTrigger):
     """CreateHyperparameterTuningJobTrigger run on the trigger worker to 
perform create operation."""
 
diff --git a/providers/google/src/airflow/providers/google/get_provider_info.py 
b/providers/google/src/airflow/providers/google/get_provider_info.py
index 61571bc6831..92966a4b367 100644
--- a/providers/google/src/airflow/providers/google/get_provider_info.py
+++ b/providers/google/src/airflow/providers/google/get_provider_info.py
@@ -672,6 +672,7 @@ def get_provider_info():
                     
"airflow.providers.google.cloud.operators.vertex_ai.generative_model",
                     
"airflow.providers.google.cloud.operators.vertex_ai.feature_store",
                     "airflow.providers.google.cloud.operators.vertex_ai.ray",
+                    
"airflow.providers.google.cloud.operators.vertex_ai.agent_engine",
                 ],
             },
             {
@@ -1043,6 +1044,7 @@ def get_provider_info():
                     
"airflow.providers.google.cloud.hooks.vertex_ai.generative_model",
                     
"airflow.providers.google.cloud.hooks.vertex_ai.prediction_service",
                     
"airflow.providers.google.cloud.hooks.vertex_ai.feature_store",
+                    
"airflow.providers.google.cloud.hooks.vertex_ai.agent_engine",
                     "airflow.providers.google.cloud.hooks.vertex_ai.ray",
                 ],
             },
diff --git 
a/providers/google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
 
b/providers/google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
new file mode 100644
index 00000000000..1f7ebb3bdfb
--- /dev/null
+++ 
b/providers/google/tests/system/google/cloud/vertex_ai/example_vertex_ai_agent_engine.py
@@ -0,0 +1,280 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Example Airflow Dag for Google Vertex AI Agent Engine operations.
+
+The test is self-contained: it builds the agent container image with Cloud 
Build,
+pushing it to an Artifact Registry repository scoped to this test run, and 
deletes
+the repository (and the GCS bucket used for query output) when the test 
finishes.
+
+One-time setup required in the test project before running the test:
+
+1. Enable the required APIs::
+
+    gcloud services enable cloudbuild.googleapis.com 
artifactregistry.googleapis.com
+
+2. Grant the Agent Engine service agent access to pull the built image and to
+   write the query job output (the service agent is created the first time
+   Agent Engine is used in the project)::
+
+    gcloud projects add-iam-policy-binding PROJECT_ID \
+        
--member="serviceAccount:service-project_num...@gcp-sa-aiplatform-re.iam.gserviceaccount.com"
 \
+        --role="roles/artifactregistry.reader"
+    gcloud projects add-iam-policy-binding PROJECT_ID \
+        
--member="serviceAccount:service-project_num...@gcp-sa-aiplatform-re.iam.gserviceaccount.com"
 \
+        --role="roles/storage.objectAdmin"
+
+3. The Cloud Build service account must be able to create and delete Artifact
+   Registry repositories (the default one can; otherwise grant it
+   ``roles/artifactregistry.admin``).
+"""
+
+from __future__ import annotations
+
+import base64
+import json
+import os
+from datetime import datetime
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from airflow.providers.google.cloud.operators.cloud_build import 
CloudBuildCreateBuildOperator
+from airflow.providers.google.cloud.operators.gcs import 
GCSCreateBucketOperator, GCSDeleteBucketOperator
+from airflow.providers.google.cloud.operators.vertex_ai.agent_engine import (
+    CreateAgentEngineOperator,
+    DeleteAgentEngineOperator,
+    GetAgentEngineOperator,
+    RunQueryJobOperator,
+    UpdateAgentEngineOperator,
+)
+
+if TYPE_CHECKING:
+    from vertexai._genai import types as vertexai_types
+
+try:
+    from airflow.sdk import DAG, TriggerRule
+except ImportError:
+    # Compatibility for Airflow < 3.1
+    from airflow.models.dag import DAG  # type: 
ignore[attr-defined,no-redef,assignment]
+    from airflow.utils.trigger_rule import TriggerRule  # type: 
ignore[no-redef,attr-defined]
+
+DAG_ID = "vertex_ai_agent_engine_operations"
+ENV_ID = os.environ.get("SYSTEM_TESTS_ENV_ID", "default")
+PROJECT_ID = os.environ.get("SYSTEM_TESTS_GCP_PROJECT", "default")
+LOCATION = "us-central1"
+
+REPOSITORY_ID = f"repo_{DAG_ID}_{ENV_ID}".replace("_", "-")
+CONTAINER_URI = 
f"{LOCATION}-docker.pkg.dev/{PROJECT_ID}/{REPOSITORY_ID}/airflow-hello-agent:latest"
+BUCKET_NAME = f"bucket_{DAG_ID}_{ENV_ID}".replace("_", "-")
+QUERY_OUTPUT_GCS_URI = f"gs://{BUCKET_NAME}/query-output/"
+DISPLAY_NAME = f"airflow-agent-engine-{ENV_ID}"
+QUERY_STR = json.dumps({"input": "hello from Airflow"})
+
+AGENT_RESOURCES_DIR = Path(__file__).parent / "resources" / "agent_engine"
+AGENT_SOURCE_B64 = base64.b64encode((AGENT_RESOURCES_DIR / 
"hello_agent.py").read_bytes()).decode()
+AGENT_DOCKERFILE_B64 = base64.b64encode((AGENT_RESOURCES_DIR / 
"Dockerfile").read_bytes()).decode()
+
+# The agent sources under resources/agent_engine/ are inlined base64-encoded 
so the
+# build needs no external source (bucket or repository) to exist beforehand.
+BUILD_AGENT_IMAGE_BODY = {
+    "steps": [
+        {
+            "name": "gcr.io/google.com/cloudsdktool/cloud-sdk:slim",
+            "entrypoint": "bash",
+            "args": [
+                "-c",
+                f"gcloud artifacts repositories describe {REPOSITORY_ID} 
--location={LOCATION} "
+                f"|| gcloud artifacts repositories create {REPOSITORY_ID} "
+                f"--repository-format=docker --location={LOCATION}",
+            ],
+        },
+        {
+            "name": "gcr.io/cloud-builders/docker",
+            "entrypoint": "bash",
+            "args": [
+                "-c",
+                f"echo {AGENT_SOURCE_B64} | base64 -d > hello_agent.py && "
+                f"echo {AGENT_DOCKERFILE_B64} | base64 -d > Dockerfile && "
+                f"docker build -t {CONTAINER_URI} .",
+            ],
+        },
+    ],
+    "images": [CONTAINER_URI],
+}
+
+DELETE_AGENT_IMAGE_BODY = {
+    "steps": [
+        {
+            "name": "gcr.io/google.com/cloudsdktool/cloud-sdk:slim",
+            "entrypoint": "bash",
+            "args": [
+                "-c",
+                f"gcloud artifacts repositories delete {REPOSITORY_ID} 
--location={LOCATION} --quiet",
+            ],
+        },
+    ],
+}
+
+AGENT_ENGINE_ID = "{{ 
task_instance.xcom_pull(task_ids='create_agent_engine')['name'].split('/')[-1] 
}}"
+
+QUERY_CONFIG: vertexai_types.RunQueryJobAgentEngineConfigDict = {
+    "query": QUERY_STR,
+    "output_gcs_uri": QUERY_OUTPUT_GCS_URI,
+}
+
+
+with DAG(
+    DAG_ID,
+    schedule="@once",
+    start_date=datetime(2021, 1, 1),
+    catchup=False,
+    tags=["example", "vertex_ai", "agent_engine"],
+) as dag:
+    create_bucket = GCSCreateBucketOperator(
+        task_id="create_bucket",
+        bucket_name=BUCKET_NAME,
+        storage_class="REGIONAL",
+        location=LOCATION,
+    )
+
+    build_agent_image = CloudBuildCreateBuildOperator(
+        task_id="build_agent_image",
+        project_id=PROJECT_ID,
+        build=BUILD_AGENT_IMAGE_BODY,
+    )
+
+    # [START how_to_cloud_vertex_ai_create_agent_engine_operator]
+    create_agent_engine = CreateAgentEngineOperator(
+        task_id="create_agent_engine",
+        project_id=PROJECT_ID,
+        location=LOCATION,
+        config={
+            "display_name": DISPLAY_NAME,
+            "description": "Airflow system test Agent Engine",
+            "agent_framework": "custom",
+            "min_instances": 0,
+            "max_instances": 1,
+            "resource_limits": {"cpu": "1", "memory": "1Gi"},
+            "container_spec": {"image_uri": CONTAINER_URI},
+            "class_methods": [
+                {
+                    "name": "query",
+                    "api_mode": "",
+                },
+            ],
+        },
+    )
+    # [END how_to_cloud_vertex_ai_create_agent_engine_operator]
+
+    # [START how_to_cloud_vertex_ai_get_agent_engine_operator]
+    get_agent_engine = GetAgentEngineOperator(
+        task_id="get_agent_engine",
+        project_id=PROJECT_ID,
+        location=LOCATION,
+        agent_engine_id=AGENT_ENGINE_ID,
+    )
+    # [END how_to_cloud_vertex_ai_get_agent_engine_operator]
+
+    # [START how_to_cloud_vertex_ai_run_query_job_operator]
+    run_query_job = RunQueryJobOperator(
+        task_id="run_query_job",
+        project_id=PROJECT_ID,
+        location=LOCATION,
+        agent_engine_id=AGENT_ENGINE_ID,
+        config=QUERY_CONFIG,
+        check_config={"retrieve_result": True},
+        poll_interval=10,
+        timeout=900,
+    )
+    # [END how_to_cloud_vertex_ai_run_query_job_operator]
+
+    # [START how_to_cloud_vertex_ai_run_query_job_operator_deferrable]
+    run_query_job_deferrable = RunQueryJobOperator(
+        task_id="run_query_job_deferrable",
+        project_id=PROJECT_ID,
+        location=LOCATION,
+        agent_engine_id=AGENT_ENGINE_ID,
+        config=QUERY_CONFIG,
+        check_config={"retrieve_result": True},
+        poll_interval=10,
+        timeout=900,
+        deferrable=True,
+    )
+    # [END how_to_cloud_vertex_ai_run_query_job_operator_deferrable]
+
+    # [START how_to_cloud_vertex_ai_update_agent_engine_operator]
+    update_agent_engine = UpdateAgentEngineOperator(
+        task_id="update_agent_engine",
+        project_id=PROJECT_ID,
+        location=LOCATION,
+        agent_engine_id=AGENT_ENGINE_ID,
+        config={
+            "display_name": f"{DISPLAY_NAME}-updated",
+            "description": "Updated Airflow system test Agent Engine",
+        },
+    )
+    # [END how_to_cloud_vertex_ai_update_agent_engine_operator]
+
+    # [START how_to_cloud_vertex_ai_delete_agent_engine_operator]
+    delete_agent_engine = DeleteAgentEngineOperator(
+        task_id="delete_agent_engine",
+        project_id=PROJECT_ID,
+        location=LOCATION,
+        agent_engine_id=AGENT_ENGINE_ID,
+        force=True,
+        trigger_rule=TriggerRule.ALL_DONE,
+    )
+    # [END how_to_cloud_vertex_ai_delete_agent_engine_operator]
+
+    delete_agent_image = CloudBuildCreateBuildOperator(
+        task_id="delete_agent_image",
+        project_id=PROJECT_ID,
+        build=DELETE_AGENT_IMAGE_BODY,
+        trigger_rule=TriggerRule.ALL_DONE,
+    )
+
+    delete_bucket = GCSDeleteBucketOperator(
+        task_id="delete_bucket",
+        bucket_name=BUCKET_NAME,
+        trigger_rule=TriggerRule.ALL_DONE,
+    )
+
+    (
+        [create_bucket, build_agent_image]
+        >> create_agent_engine
+        >> get_agent_engine
+        >> run_query_job
+        >> run_query_job_deferrable
+        >> update_agent_engine
+        >> delete_agent_engine
+        >> [delete_agent_image, delete_bucket]
+    )
+
+    # ### Everything below this line is not part of example ###
+    # ### Just for system tests purpose ###
+    from tests_common.test_utils.watcher import watcher
+
+    # This test needs watcher in order to properly mark success/failure
+    # when "tearDown" task with trigger rule is part of the Dag
+    list(dag.tasks) >> watcher()
+
+from tests_common.test_utils.system_tests import get_test_run  # noqa: E402
+
+# Needed to run the example Dag with pytest (see: 
contributing-docs/testing/system_tests.rst)
+test_run = get_test_run(dag)
diff --git 
a/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/Dockerfile
 
b/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/Dockerfile
new file mode 100644
index 00000000000..fcc08b55d41
--- /dev/null
+++ 
b/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/Dockerfile
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+FROM python:3.12-slim
+
+ENV PYTHONUNBUFFERED=1
+
+WORKDIR /app
+
+COPY hello_agent.py .
+
+EXPOSE 8080
+
+CMD ["python", "hello_agent.py"]
diff --git 
a/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/__init__.py
 
b/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ 
b/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git 
a/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/hello_agent.py
 
b/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/hello_agent.py
new file mode 100644
index 00000000000..4ff450dc8bf
--- /dev/null
+++ 
b/providers/google/tests/system/google/cloud/vertex_ai/resources/agent_engine/hello_agent.py
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Minimal agent served from the custom container used by the Agent Engine 
system test."""
+
+from __future__ import annotations
+
+import contextlib
+import json
+import os
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from typing import Any
+
+
+class _InvocationHandler(BaseHTTPRequestHandler):
+    """Serve health checks and query requests for the Agent Engine system 
test."""
+
+    def do_GET(self) -> None:
+        self._send_json(200, {"ok": True})
+
+    def do_POST(self) -> None:
+        input_value = None
+        with contextlib.suppress(Exception):
+            length = int(self.headers.get("content-length", "0"))
+            payload = json.loads(self.rfile.read(length) or b"{}")
+            input_value = payload.get("input", payload)
+            if isinstance(input_value, str):
+                input_value = json.loads(input_value)
+        self._send_json(200, {"output": {"message": "Hello from Agent Engine", 
"received": input_value}})
+
+    def _send_json(self, status: int, body: dict[str, Any]) -> None:
+        data = json.dumps(body).encode()
+        self.send_response(status)
+        self.send_header("content-type", "application/json")
+        self.send_header("content-length", str(len(data)))
+        self.end_headers()
+        self.wfile.write(data)
+
+
+if __name__ == "__main__":
+    port = int(os.environ.get("PORT", "8080"))
+    HTTPServer(("0.0.0.0", port), _InvocationHandler).serve_forever()
diff --git 
a/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_agent_engine.py 
b/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_agent_engine.py
new file mode 100644
index 00000000000..54574865612
--- /dev/null
+++ 
b/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_agent_engine.py
@@ -0,0 +1,385 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+from airflow.providers.google.cloud.hooks.vertex_ai.agent_engine import 
AgentEngineAsyncHook, AgentEngineHook
+
+from unit.google.cloud.utils.base_gcp_mock import 
mock_base_gcp_hook_default_project_id
+
+BASE_STRING = "airflow.providers.google.common.hooks.base_google.{}"
+AGENT_ENGINE_STRING = 
"airflow.providers.google.cloud.hooks.vertex_ai.agent_engine.{}"
+
+TEST_GCP_CONN_ID = "test-gcp-conn-id"
+GCP_PROJECT = "test-project"
+GCP_LOCATION = "us-central1"
+AGENT_ENGINE_ID = "123"
+AGENT_ENGINE_NAME = 
"projects/test-project/locations/us-central1/reasoningEngines/123"
+OPERATION_NAME = 
"projects/test-project/locations/us-central1/operations/delete-123"
+QUERY_OPERATION_NAME = 
"projects/test-project/locations/us-central1/operations/query-123"
+OPERATION_ID = "delete-123"
+QUERY_OPERATION_ID = "query-123"
+CONFIG = {"display_name": "test-agent-engine"}
+QUERY_CONFIG = {"query": "hello", "output_gcs_uri": 
"gs://test-bucket/query-output/"}
+CHECK_QUERY_CONFIG = {"retrieve_result": True}
+
+
+class TestAgentEngineHookWithDefaultProjectId:
+    def setup_method(self):
+        with mock.patch(
+            BASE_STRING.format("GoogleBaseHook.__init__"), 
new=mock_base_gcp_hook_default_project_id
+        ):
+            self.hook = AgentEngineHook(gcp_conn_id=TEST_GCP_CONN_ID)
+
+    @mock.patch(AGENT_ENGINE_STRING.format("Client"), autospec=True)
+    def test_get_agent_engine_client(self, mock_client):
+        self.hook.get_credentials = 
mock.Mock(return_value=mock.sentinel.credentials, spec=())
+
+        result = self.hook.get_agent_engine_client(project_id=GCP_PROJECT, 
location=GCP_LOCATION)
+
+        mock_client.assert_called_once_with(
+            project=GCP_PROJECT,
+            location=GCP_LOCATION,
+            credentials=mock.sentinel.credentials,
+        )
+        assert result == mock_client.return_value.agent_engines
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_client"),
 autospec=True)
+    def test_create_agent_engine(self, mock_get_client):
+        result = self.hook.create_agent_engine(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            config=CONFIG,
+        )
+
+        mock_get_client.assert_called_once_with(self.hook, 
project_id=GCP_PROJECT, location=GCP_LOCATION)
+        mock_get_client.return_value.create.assert_called_once_with(
+            agent=None,
+            config=CONFIG,
+        )
+        assert result == mock_get_client.return_value.create.return_value
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_client"),
 autospec=True)
+    def test_get_agent_engine(self, mock_get_client):
+        result = self.hook.get_agent_engine(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+        )
+
+        
mock_get_client.return_value.get.assert_called_once_with(name=AGENT_ENGINE_NAME,
 config=None)
+        assert result == mock_get_client.return_value.get.return_value
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_client"),
 autospec=True)
+    def test_get_agent_engine_with_config(self, mock_get_client):
+        result = self.hook.get_agent_engine(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=CONFIG,
+        )
+
+        
mock_get_client.return_value.get.assert_called_once_with(name=AGENT_ENGINE_NAME,
 config=CONFIG)
+        assert result == mock_get_client.return_value.get.return_value
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_client"),
 autospec=True)
+    def test_run_query_job(self, mock_get_client):
+        result = self.hook.run_query_job(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=QUERY_CONFIG,
+        )
+
+        mock_get_client.return_value.run_query_job.assert_called_once_with(
+            name=AGENT_ENGINE_NAME,
+            config=QUERY_CONFIG,
+        )
+        assert result == 
mock_get_client.return_value.run_query_job.return_value
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_client"),
 autospec=True)
+    def test_check_query_agent_engine_job(self, mock_get_client):
+        result = self.hook.check_query_agent_engine_job(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+        )
+
+        mock_get_client.return_value.check_query_job.assert_called_once_with(
+            name=QUERY_OPERATION_NAME,
+            config=CHECK_QUERY_CONFIG,
+        )
+        assert result == 
mock_get_client.return_value.check_query_job.return_value
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.check_query_agent_engine_job"),
 autospec=True)
+    def test_wait_for_query_agent_engine_job_returns_when_successful(self, 
mock_check_query_job):
+        mock_check_query_job.return_value.status = "SUCCESS"
+
+        result = self.hook.wait_for_query_agent_engine_job(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+        )
+
+        mock_check_query_job.assert_called_once_with(
+            self.hook,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+        )
+        assert result == mock_check_query_job.return_value
+
+    @mock.patch(AGENT_ENGINE_STRING.format("time.sleep"), autospec=True)
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.check_query_agent_engine_job"),
 autospec=True)
+    def test_wait_for_query_agent_engine_job_polls_until_success(self, 
mock_check_query_job, mock_sleep):
+        running_job = mock.Mock(status="RUNNING")
+        success_job = mock.Mock(status="SUCCESS")
+        mock_check_query_job.side_effect = [running_job, running_job, 
success_job]
+
+        result = self.hook.wait_for_query_agent_engine_job(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+            poll_interval=10,
+        )
+
+        assert result is success_job
+        assert mock_check_query_job.call_count == 3
+        assert mock_sleep.call_count == 2
+        mock_sleep.assert_called_with(10)
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.check_query_agent_engine_job"),
 autospec=True)
+    def test_wait_for_query_agent_engine_job_raises_on_failed_status(self, 
mock_check_query_job):
+        mock_check_query_job.return_value.status = "FAILED"
+
+        with pytest.raises(RuntimeError, match="Agent Engine query job .* 
failed"):
+            self.hook.wait_for_query_agent_engine_job(
+                project_id=GCP_PROJECT,
+                location=GCP_LOCATION,
+                operation_id=QUERY_OPERATION_ID,
+                config=CHECK_QUERY_CONFIG,
+            )
+
+    @mock.patch(AGENT_ENGINE_STRING.format("time.sleep"), autospec=True)
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.check_query_agent_engine_job"),
 autospec=True)
+    def test_wait_for_query_agent_engine_job_raises_on_unexpected_status(
+        self, mock_check_query_job, mock_sleep
+    ):
+        mock_check_query_job.return_value.status = "CANCELLED"
+
+        with pytest.raises(
+            RuntimeError,
+            match=f"Agent Engine query job {QUERY_OPERATION_NAME} completed 
with unexpected status CANCELLED.",
+        ):
+            self.hook.wait_for_query_agent_engine_job(
+                project_id=GCP_PROJECT,
+                location=GCP_LOCATION,
+                operation_id=QUERY_OPERATION_ID,
+                config=CHECK_QUERY_CONFIG,
+            )
+
+        mock_sleep.assert_not_called()
+
+    @mock.patch(AGENT_ENGINE_STRING.format("time.sleep"), autospec=True)
+    @mock.patch(AGENT_ENGINE_STRING.format("time.monotonic"), autospec=True)
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.check_query_agent_engine_job"),
 autospec=True)
+    def test_wait_for_query_agent_engine_job_times_out(
+        self, mock_check_query_job, mock_monotonic, mock_sleep
+    ):
+        mock_check_query_job.return_value.status = "RUNNING"
+        mock_monotonic.side_effect = [1, 3]
+
+        with pytest.raises(TimeoutError, match="Timed out waiting for Agent 
Engine query job"):
+            self.hook.wait_for_query_agent_engine_job(
+                project_id=GCP_PROJECT,
+                location=GCP_LOCATION,
+                operation_id=QUERY_OPERATION_ID,
+                config=CHECK_QUERY_CONFIG,
+                timeout=1,
+            )
+
+        mock_sleep.assert_not_called()
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_client"),
 autospec=True)
+    def test_update_agent_engine(self, mock_get_client):
+        result = self.hook.update_agent_engine(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=CONFIG,
+        )
+
+        mock_get_client.return_value.update.assert_called_once_with(
+            name=AGENT_ENGINE_NAME,
+            agent=None,
+            config=CONFIG,
+        )
+        assert result == mock_get_client.return_value.update.return_value
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_client"),
 autospec=True)
+    def test_delete_agent_engine(self, mock_get_client):
+        result = self.hook.delete_agent_engine(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            force=True,
+            config=CONFIG,
+        )
+
+        mock_get_client.return_value.delete.assert_called_once_with(
+            name=AGENT_ENGINE_NAME,
+            force=True,
+            config=CONFIG,
+        )
+        assert result == mock_get_client.return_value.delete.return_value
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("google.auth.transport.requests.AuthorizedSession"),
 autospec=True)
+    def test_get_agent_engine_operation(self, mock_session):
+        self.hook.get_credentials = 
mock.Mock(return_value=mock.sentinel.credentials, spec=())
+        mock_session.return_value.get.return_value.json.return_value = 
{"name": OPERATION_NAME, "done": True}
+
+        result = self.hook.get_agent_engine_operation(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=OPERATION_ID,
+        )
+
+        mock_session.assert_called_once_with(mock.sentinel.credentials)
+        mock_session.return_value.get.assert_called_once_with(
+            
f"https://{GCP_LOCATION}-aiplatform.googleapis.com/v1beta1/{OPERATION_NAME}";,
+            timeout=60.0,
+        )
+        
mock_session.return_value.get.return_value.raise_for_status.assert_called_once_with()
+        assert result == {"name": OPERATION_NAME, "done": True}
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("google.auth.transport.requests.AuthorizedSession"),
 autospec=True)
+    def test_get_agent_engine_operation_with_request_timeout(self, 
mock_session):
+        self.hook.get_credentials = 
mock.Mock(return_value=mock.sentinel.credentials, spec=())
+        mock_session.return_value.get.return_value.json.return_value = 
{"name": OPERATION_NAME, "done": True}
+
+        self.hook.get_agent_engine_operation(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=OPERATION_ID,
+            request_timeout=10,
+        )
+
+        mock_session.return_value.get.assert_called_once_with(
+            
f"https://{GCP_LOCATION}-aiplatform.googleapis.com/v1beta1/{OPERATION_NAME}";,
+            timeout=10,
+        )
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_operation"),
 autospec=True)
+    def test_wait_for_agent_engine_operation_returns_when_done(self, 
mock_get_operation):
+        mock_get_operation.return_value = {"name": OPERATION_NAME, "done": 
True}
+
+        self.hook.wait_for_agent_engine_operation(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=OPERATION_ID,
+        )
+
+        mock_get_operation.assert_called_once_with(
+            self.hook,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=OPERATION_ID,
+        )
+
+    @mock.patch(AGENT_ENGINE_STRING.format("time.sleep"), autospec=True)
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_operation"),
 autospec=True)
+    def test_wait_for_agent_engine_operation_polls_until_done(self, 
mock_get_operation, mock_sleep):
+        running_operation = {"name": OPERATION_NAME, "done": False}
+        done_operation = {"name": OPERATION_NAME, "done": True}
+        mock_get_operation.side_effect = [running_operation, 
running_operation, done_operation]
+
+        self.hook.wait_for_agent_engine_operation(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=OPERATION_ID,
+            poll_interval=10,
+        )
+
+        assert mock_get_operation.call_count == 3
+        assert mock_sleep.call_count == 2
+        mock_sleep.assert_called_with(10)
+
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_operation"),
 autospec=True)
+    def test_wait_for_agent_engine_operation_raises_on_error(self, 
mock_get_operation):
+        mock_get_operation.return_value = {"name": OPERATION_NAME, "done": 
True, "error": {"message": "boom"}}
+
+        with pytest.raises(RuntimeError, match="Agent Engine operation .* 
failed"):
+            self.hook.wait_for_agent_engine_operation(
+                project_id=GCP_PROJECT,
+                location=GCP_LOCATION,
+                operation_id=OPERATION_ID,
+            )
+
+    @mock.patch(AGENT_ENGINE_STRING.format("time.sleep"), autospec=True)
+    @mock.patch(AGENT_ENGINE_STRING.format("time.monotonic"), autospec=True)
+    
@mock.patch(AGENT_ENGINE_STRING.format("AgentEngineHook.get_agent_engine_operation"),
 autospec=True)
+    def test_wait_for_agent_engine_operation_times_out(self, 
mock_get_operation, mock_monotonic, mock_sleep):
+        mock_get_operation.return_value = {"name": OPERATION_NAME, "done": 
False}
+        mock_monotonic.side_effect = [1, 3]
+
+        with pytest.raises(TimeoutError, match="Timed out waiting for Agent 
Engine operation"):
+            self.hook.wait_for_agent_engine_operation(
+                project_id=GCP_PROJECT,
+                location=GCP_LOCATION,
+                operation_id=OPERATION_ID,
+                timeout=1,
+            )
+
+        mock_sleep.assert_not_called()
+
+
+class TestAgentEngineAsyncHook:
+    def setup_method(self):
+        with mock.patch(
+            BASE_STRING.format("GoogleBaseAsyncHook.__init__"),
+            return_value=None,
+        ):
+            self.hook = AgentEngineAsyncHook(gcp_conn_id=TEST_GCP_CONN_ID)
+
+    @pytest.mark.asyncio
+    async def test_check_query_agent_engine_job_calls_sync_hook(self):
+        sync_hook = mock.Mock(spec=AgentEngineHook)
+        sync_hook.check_query_agent_engine_job.return_value = 
mock.sentinel.query_job
+        self.hook.get_sync_hook = mock.AsyncMock(return_value=sync_hook)
+
+        result = await self.hook.check_query_agent_engine_job(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+        )
+
+        sync_hook.check_query_agent_engine_job.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+        )
+        assert result == mock.sentinel.query_job
diff --git 
a/providers/google/tests/unit/google/cloud/operators/vertex_ai/test_agent_engine.py
 
b/providers/google/tests/unit/google/cloud/operators/vertex_ai/test_agent_engine.py
new file mode 100644
index 00000000000..586eead4c77
--- /dev/null
+++ 
b/providers/google/tests/unit/google/cloud/operators/vertex_ai/test_agent_engine.py
@@ -0,0 +1,465 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+from airflow.providers.common.compat.sdk import TaskDeferred
+from airflow.providers.google.cloud.operators.vertex_ai.agent_engine import (
+    CreateAgentEngineOperator,
+    DeleteAgentEngineOperator,
+    GetAgentEngineOperator,
+    RunQueryJobOperator,
+    UpdateAgentEngineOperator,
+)
+
+AGENT_ENGINE_PATH = 
"airflow.providers.google.cloud.operators.vertex_ai.agent_engine.{}"
+
+TASK_ID = "test_task_id"
+GCP_PROJECT = "test-project"
+GCP_LOCATION = "us-central1"
+GCP_CONN_ID = "test-conn"
+IMPERSONATION_CHAIN = ["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"]
+AGENT_ENGINE_ID = "123"
+AGENT_ENGINE_NAME = 
"projects/test-project/locations/us-central1/reasoningEngines/123"
+CONFIG = {"display_name": "test-agent-engine"}
+QUERY_CONFIG = {"query": "hello", "output_gcs_uri": 
"gs://test-bucket/query-output/"}
+CHECK_QUERY_CONFIG = {"retrieve_result": True}
+OPERATION = {"name": "operations/delete-123", "done": False}
+QUERY_OPERATION_NAME = "operations/query-123"
+OPERATION_ID = "delete-123"
+QUERY_OPERATION_ID = "query-123"
+
+
+class FakeModel:
+    def __init__(self, payload):
+        self.payload = payload
+        for key, value in payload.items():
+            setattr(self, key, value)
+
+    def model_dump(self, mode="json"):
+        return self.payload
+
+
+class FakeAgentEngine:
+    def __init__(self, payload):
+        self.api_resource = FakeModel(payload)
+
+
[email protected]
+def context():
+    return {"ti": mock.Mock(spec_set=["xcom_push"])}
+
+
+def assert_hook_created(mock_hook):
+    mock_hook.assert_called_once_with(
+        gcp_conn_id=GCP_CONN_ID,
+        impersonation_chain=IMPERSONATION_CHAIN,
+    )
+
+
+class TestCreateAgentEngineOperator:
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute(self, mock_hook, context):
+        mock_hook.return_value.create_agent_engine.return_value = 
FakeAgentEngine(
+            {"name": AGENT_ENGINE_NAME, "display_name": "test-agent-engine"}
+        )
+        op = CreateAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            config=CONFIG,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        assert_hook_created(mock_hook)
+        mock_hook.return_value.create_agent_engine.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent=None,
+            config=CONFIG,
+        )
+        assert result == {"name": AGENT_ENGINE_NAME, "display_name": 
"test-agent-engine"}
+
+
+class TestGetAgentEngineOperator:
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute(self, mock_hook, context):
+        mock_hook.return_value.get_agent_engine.return_value = 
FakeAgentEngine({"name": AGENT_ENGINE_NAME})
+        op = GetAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        mock_hook.return_value.get_agent_engine.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=None,
+        )
+        assert result == {"name": AGENT_ENGINE_NAME}
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_with_config(self, mock_hook, context):
+        mock_hook.return_value.get_agent_engine.return_value = 
FakeAgentEngine({"name": AGENT_ENGINE_NAME})
+        op = GetAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=CONFIG,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        op.execute(context=context)
+
+        mock_hook.return_value.get_agent_engine.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=CONFIG,
+        )
+
+
+class TestRunQueryJobOperator:
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute(self, mock_hook, context):
+        run_result_payload = {
+            "job_name": "operations/query-123",
+            "input_gcs_uri": "gs://test-bucket/query-output/input.json",
+            "output_gcs_uri": "gs://test-bucket/query-output/output.json",
+        }
+        query_result_payload = {
+            "operation_name": QUERY_OPERATION_NAME,
+            "output_gcs_uri": "gs://test-bucket/query-output/output.json",
+            "status": "SUCCESS",
+            "result": "done",
+        }
+        mock_hook.return_value.run_query_job.return_value = 
FakeModel(run_result_payload)
+        mock_hook.return_value.wait_for_query_agent_engine_job.return_value = 
FakeModel(query_result_payload)
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=QUERY_CONFIG,
+            check_config=CHECK_QUERY_CONFIG,
+            poll_interval=1,
+            timeout=60,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        mock_hook.return_value.run_query_job.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=QUERY_CONFIG,
+        )
+        
mock_hook.return_value.wait_for_query_agent_engine_job.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+            poll_interval=1,
+            timeout=60,
+        )
+        assert result == query_result_payload
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_without_wait(self, mock_hook, context):
+        result_payload = {
+            "job_name": "operations/query-123",
+            "input_gcs_uri": "gs://test-bucket/query-output/input.json",
+            "output_gcs_uri": "gs://test-bucket/query-output/output.json",
+        }
+        mock_hook.return_value.run_query_job.return_value = 
FakeModel(result_payload)
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            config=CHECK_QUERY_CONFIG,
+            agent_engine_id=AGENT_ENGINE_ID,
+            wait_for_completion=False,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        
mock_hook.return_value.wait_for_query_agent_engine_job.assert_not_called()
+        assert result == result_payload
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineQueryJobTrigger"), 
autospec=True)
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_deferrable(self, mock_hook, mock_trigger, context):
+        mock_hook.return_value.run_query_job.return_value = 
FakeModel({"job_name": QUERY_OPERATION_NAME})
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=CHECK_QUERY_CONFIG,
+            check_config=CHECK_QUERY_CONFIG,
+            poll_interval=1,
+            timeout=60,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            deferrable=True,
+        )
+
+        with pytest.raises(TaskDeferred):
+            op.execute(context=context)
+
+        
mock_hook.return_value.wait_for_query_agent_engine_job.assert_not_called()
+        mock_trigger.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            poll_interval=1,
+            timeout=60,
+        )
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_raises_when_query_job_has_no_name(self, mock_hook, 
context):
+        mock_hook.return_value.run_query_job.return_value = FakeModel({})
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+        )
+
+        with pytest.raises(RuntimeError, match="Agent Engine query job did not 
include an operation name."):
+            op.execute(context=context)
+
+        
mock_hook.return_value.wait_for_query_agent_engine_job.assert_not_called()
+
+    def test_execute_complete_success(self, context):
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+        )
+        query_job = {"operation_name": QUERY_OPERATION_NAME, "status": 
"SUCCESS"}
+
+        result = op.execute_complete(
+            context=context,
+            event={"status": "success", "message": "done", "query_job": 
query_job},
+        )
+
+        assert result == query_job
+
+    def test_execute_complete_error(self, context):
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+        )
+
+        with pytest.raises(RuntimeError, match="boom"):
+            op.execute_complete(context=context, event={"status": "error", 
"message": "boom"})
+
+    def test_execute_complete_timeout(self, context):
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+        )
+
+        with pytest.raises(TimeoutError, match="timed out"):
+            op.execute_complete(context=context, event={"status": "timeout", 
"message": "timed out"})
+
+    def test_execute_complete_without_event(self, context):
+        op = RunQueryJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+        )
+
+        with pytest.raises(RuntimeError, match="No event received in trigger 
callback"):
+            op.execute_complete(context=context)
+
+
+class TestUpdateAgentEngineOperator:
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute(self, mock_hook, context):
+        mock_hook.return_value.update_agent_engine.return_value = 
FakeAgentEngine(
+            {"name": AGENT_ENGINE_NAME, "display_name": "updated-agent-engine"}
+        )
+        op = UpdateAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            config=CONFIG,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        mock_hook.return_value.update_agent_engine.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            agent=None,
+            config=CONFIG,
+        )
+        assert result == {"name": AGENT_ENGINE_NAME, "display_name": 
"updated-agent-engine"}
+
+
+class TestDeleteAgentEngineOperator:
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_without_wait(self, mock_hook, context):
+        mock_hook.return_value.delete_agent_engine.return_value = 
FakeModel(OPERATION)
+        op = DeleteAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            force=True,
+            config=CONFIG,
+            wait_for_completion=False,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        mock_hook.return_value.delete_agent_engine.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            force=True,
+            config=CONFIG,
+        )
+        
mock_hook.return_value.wait_for_agent_engine_operation.assert_not_called()
+        assert result == OPERATION
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_waits_until_deleted(self, mock_hook, context):
+        mock_hook.return_value.delete_agent_engine.return_value = 
FakeModel(OPERATION)
+        op = DeleteAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            wait_for_completion=True,
+            poll_interval=1,
+            timeout=60,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        
mock_hook.return_value.wait_for_agent_engine_operation.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=OPERATION_ID,
+            poll_interval=1,
+            timeout=60,
+        )
+        assert result == OPERATION
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_does_not_wait_when_delete_operation_is_done(self, 
mock_hook, context):
+        operation = {"name": "operations/delete-123", "done": True}
+        mock_hook.return_value.delete_agent_engine.return_value = 
FakeModel(operation)
+        op = DeleteAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            wait_for_completion=True,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        result = op.execute(context=context)
+
+        
mock_hook.return_value.wait_for_agent_engine_operation.assert_not_called()
+        assert result == operation
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_raises_when_completed_delete_operation_has_error(self, 
mock_hook, context):
+        operation = {
+            "name": "operations/delete-123",
+            "done": True,
+            "error": {"message": "Permission denied"},
+        }
+        mock_hook.return_value.delete_agent_engine.return_value = 
FakeModel(operation)
+        op = DeleteAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            wait_for_completion=True,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        with pytest.raises(
+            RuntimeError,
+            match=r"Agent Engine operation operations/delete-123 failed: 
\{'message': 'Permission denied'\}",
+        ):
+            op.execute(context=context)
+
+        
mock_hook.return_value.wait_for_agent_engine_operation.assert_not_called()
+
+    @mock.patch(AGENT_ENGINE_PATH.format("AgentEngineHook"), autospec=True)
+    def test_execute_raises_when_delete_operation_has_no_name(self, mock_hook, 
context):
+        mock_hook.return_value.delete_agent_engine.return_value = 
FakeModel({"done": False})
+        op = DeleteAgentEngineOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            agent_engine_id=AGENT_ENGINE_ID,
+            wait_for_completion=True,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+
+        with pytest.raises(
+            RuntimeError, match=r"Delete Agent Engine operation did not 
include an operation name\."
+        ):
+            op.execute(context=context)
+
+        
mock_hook.return_value.wait_for_agent_engine_operation.assert_not_called()
diff --git 
a/providers/google/tests/unit/google/cloud/triggers/test_vertex_ai_agent_engine.py
 
b/providers/google/tests/unit/google/cloud/triggers/test_vertex_ai_agent_engine.py
new file mode 100644
index 00000000000..dfb2b418be5
--- /dev/null
+++ 
b/providers/google/tests/unit/google/cloud/triggers/test_vertex_ai_agent_engine.py
@@ -0,0 +1,207 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+from airflow.providers.google.cloud.triggers.vertex_ai import 
AgentEngineQueryJobTrigger
+from airflow.triggers.base import TriggerEvent
+
+GCP_PROJECT = "test-project"
+GCP_LOCATION = "us-central1"
+GCP_CONN_ID = "test-conn"
+IMPERSONATION_CHAIN = ["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"]
+AGENT_ENGINE_ID = "123"
+QUERY_OPERATION_NAME = 
"projects/test-project/locations/us-central1/operations/query-123"
+QUERY_OPERATION_ID = "query-123"
+CHECK_QUERY_CONFIG = {"retrieve_result": True}
+
+
+class FakeModel:
+    def __init__(self, payload):
+        self.payload = payload
+        for key, value in payload.items():
+            setattr(self, key, value)
+
+    def model_dump(self, mode="json"):
+        return self.payload
+
+
[email protected]
+def query_job_trigger():
+    return AgentEngineQueryJobTrigger(
+        project_id=GCP_PROJECT,
+        location=GCP_LOCATION,
+        operation_id=QUERY_OPERATION_ID,
+        config=CHECK_QUERY_CONFIG,
+        gcp_conn_id=GCP_CONN_ID,
+        impersonation_chain=IMPERSONATION_CHAIN,
+        poll_interval=1,
+        timeout=60,
+    )
+
+
+class TestAgentEngineQueryJobTrigger:
+    def test_serialize(self, query_job_trigger):
+        assert query_job_trigger.serialize() == (
+            
"airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineQueryJobTrigger",
+            {
+                "project_id": GCP_PROJECT,
+                "location": GCP_LOCATION,
+                "operation_id": QUERY_OPERATION_ID,
+                "config": CHECK_QUERY_CONFIG,
+                "gcp_conn_id": GCP_CONN_ID,
+                "impersonation_chain": IMPERSONATION_CHAIN,
+                "poll_interval": 1,
+                "timeout": 60,
+            },
+        )
+
+    def test_serialize_with_pydantic_config(self):
+        pydantic_config = FakeModel(CHECK_QUERY_CONFIG)
+        trigger = AgentEngineQueryJobTrigger(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=pydantic_config,
+            gcp_conn_id=GCP_CONN_ID,
+            poll_interval=1,
+        )
+        _, kwargs = trigger.serialize()
+        assert kwargs["config"] == CHECK_QUERY_CONFIG
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineAsyncHook",
 autospec=True)
+    async def test_run_loop_return_success_event(self, mock_hook, 
query_job_trigger):
+        query_job = {
+            "operation_name": QUERY_OPERATION_NAME,
+            "output_gcs_uri": "gs://test-bucket/query-output/output.json",
+            "status": "SUCCESS",
+            "result": "done",
+        }
+        mock_hook.return_value.check_query_agent_engine_job.return_value = 
FakeModel(query_job)
+
+        event = await query_job_trigger.run().asend(None)
+
+        
mock_hook.return_value.check_query_agent_engine_job.assert_called_once_with(
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            operation_id=QUERY_OPERATION_ID,
+            config=CHECK_QUERY_CONFIG,
+        )
+        assert event == TriggerEvent(
+            {
+                "status": "success",
+                "message": "Agent Engine query job completed",
+                "query_job": query_job,
+            }
+        )
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.asyncio.sleep", 
autospec=True)
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineAsyncHook",
 autospec=True)
+    async def test_run_loop_polls_until_success(self, mock_hook, mock_sleep, 
query_job_trigger):
+        running_job = FakeModel({"operation_name": QUERY_OPERATION_NAME, 
"status": "RUNNING"})
+        success_job = FakeModel({"operation_name": QUERY_OPERATION_NAME, 
"status": "SUCCESS"})
+        mock_hook.return_value.check_query_agent_engine_job.side_effect = [
+            running_job,
+            running_job,
+            success_job,
+        ]
+
+        event = await query_job_trigger.run().asend(None)
+
+        assert mock_hook.return_value.check_query_agent_engine_job.call_count 
== 3
+        assert mock_sleep.call_count == 2
+        mock_sleep.assert_awaited_with(1)
+        assert event == TriggerEvent(
+            {
+                "status": "success",
+                "message": "Agent Engine query job completed",
+                "query_job": {"operation_name": QUERY_OPERATION_NAME, 
"status": "SUCCESS"},
+            }
+        )
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineAsyncHook",
 autospec=True)
+    async def test_run_loop_return_failed_event(self, mock_hook, 
query_job_trigger):
+        query_job = {"operation_name": QUERY_OPERATION_NAME, "status": 
"FAILED"}
+        mock_hook.return_value.check_query_agent_engine_job.return_value = 
FakeModel(query_job)
+
+        event = await query_job_trigger.run().asend(None)
+
+        assert event == TriggerEvent(
+            {
+                "status": "error",
+                "message": f"Agent Engine query job {QUERY_OPERATION_ID} 
failed.",
+                "query_job": query_job,
+            }
+        )
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.asyncio.sleep", 
autospec=True)
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineAsyncHook",
 autospec=True)
+    async def test_run_loop_return_timeout_event(self, mock_hook, mock_sleep, 
query_job_trigger):
+        query_job_trigger.timeout = -1
+        mock_hook.return_value.check_query_agent_engine_job.return_value = 
FakeModel({"status": "RUNNING"})
+
+        event = await query_job_trigger.run().asend(None)
+
+        mock_sleep.assert_not_called()
+        assert event == TriggerEvent(
+            {
+                "status": "timeout",
+                "message": f"Timed out waiting for Agent Engine query job 
{QUERY_OPERATION_ID}",
+                "query_job": {"status": "RUNNING"},
+            }
+        )
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineAsyncHook",
 autospec=True)
+    async def test_run_loop_return_error_event_for_unexpected_status(self, 
mock_hook, query_job_trigger):
+        query_job = {"operation_name": QUERY_OPERATION_NAME, "status": 
"CANCELLED"}
+        mock_hook.return_value.check_query_agent_engine_job.return_value = 
FakeModel(query_job)
+
+        event = await query_job_trigger.run().asend(None)
+
+        assert event == TriggerEvent(
+            {
+                "status": "error",
+                "message": (
+                    f"Agent Engine query job {QUERY_OPERATION_ID} completed 
with unexpected status CANCELLED."
+                ),
+                "query_job": query_job,
+            }
+        )
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.triggers.vertex_ai.AgentEngineAsyncHook",
 autospec=True)
+    async def test_run_loop_return_error_event(self, mock_hook, 
query_job_trigger):
+        mock_hook.return_value.check_query_agent_engine_job.side_effect = 
RuntimeError("boom")
+
+        event = await query_job_trigger.run().asend(None)
+
+        assert event == TriggerEvent(
+            {
+                "status": "error",
+                "message": "Failed while polling Agent Engine query job: boom",
+                "query_job": {"operation_id": QUERY_OPERATION_ID},
+            }
+        )
diff --git a/uv.lock b/uv.lock
index 2b08c898b6c..d055eadf726 100644
--- a/uv.lock
+++ b/uv.lock
@@ -5515,6 +5515,7 @@ dependencies = [
     { name = "google-cloud-videointelligence" },
     { name = "google-cloud-vision" },
     { name = "google-cloud-workflows" },
+    { name = "google-genai" },
     { name = "grpcio-gcp" },
     { name = "httpx" },
     { name = "immutabledict" },
@@ -5708,6 +5709,7 @@ requires-dist = [
     { name = "google-cloud-videointelligence", specifier = ">=2.11.0" },
     { name = "google-cloud-vision", specifier = ">=3.4.0" },
     { name = "google-cloud-workflows", specifier = ">=1.10.0" },
+    { name = "google-genai", specifier = ">=2.8.0" },
     { name = "grpcio-gcp", specifier = ">=0.2.2" },
     { name = "httpx", specifier = ">=0.25.0" },
     { name = "immutabledict", specifier = ">=4.2.0" },

Reply via email to