[dv, push pull agent] Support for 4-phase req-ack

- Add support for 4-phase req-ack
- add `pull_handshake_e` type
- Update cfg, agent, monitor, driver and interface to support this
- Refactor and minor fixups in monitor and sequences

Signed-off-by: Srikrishna Iyer <sriyer@google.com>
diff --git a/hw/dv/sv/push_pull_agent/push_pull_agent.sv b/hw/dv/sv/push_pull_agent/push_pull_agent.sv
index 6e26b03..e9c9910 100644
--- a/hw/dv/sv/push_pull_agent/push_pull_agent.sv
+++ b/hw/dv/sv/push_pull_agent/push_pull_agent.sv
@@ -28,9 +28,10 @@
         this, "", "vif", cfg.vif)) begin
       `uvm_fatal(`gfn, "failed to get push_pull_if handle from uvm_config_db")
     end
-    cfg.vif.if_mode               = cfg.if_mode;
-    cfg.vif.is_push_agent         = (cfg.agent_type == PushAgent);
+    cfg.vif.if_mode = cfg.if_mode;
+    cfg.vif.is_push_agent = (cfg.agent_type == PushAgent);
     cfg.vif.in_bidirectional_mode = cfg.in_bidirectional_mode;
+    cfg.vif.is_pull_handshake_4_phase = (cfg.pull_handshake_type == FourPhase);
   endfunction
 
   function void connect_phase(uvm_phase phase);
@@ -42,7 +43,7 @@
 
   virtual task run_phase(uvm_phase phase);
     push_pull_device_seq#(HostDataWidth, DeviceDataWidth) m_seq =
-      push_pull_device_seq#(HostDataWidth, DeviceDataWidth)::type_id::create("m_seq", this);
+        push_pull_device_seq#(HostDataWidth, DeviceDataWidth)::type_id::create("m_seq", this);
     if (cfg.if_mode == dv_utils_pkg::Device && cfg.start_default_device_seq) begin
       uvm_config_db#(uvm_object_wrapper)::set(null, {sequencer.get_full_name(), ".run_phase"},
                                               "default_sequence", m_seq.get_type());
diff --git a/hw/dv/sv/push_pull_agent/push_pull_agent_cfg.sv b/hw/dv/sv/push_pull_agent/push_pull_agent_cfg.sv
index 5d8ac45..475436e 100644
--- a/hw/dv/sv/push_pull_agent/push_pull_agent_cfg.sv
+++ b/hw/dv/sv/push_pull_agent/push_pull_agent_cfg.sv
@@ -13,24 +13,31 @@
   // Should be set from the IP level environment.
   push_pull_agent_e agent_type;
 
+  // Indicates the type of req-ack handshake.
+  pull_handshake_e pull_handshake_type;
+
   // Configures the agent to act in bidirectional mode,
   // transferring data on both sides of the handshake.
-  bit in_bidirectional_mode = 1'b0;
+  bit in_bidirectional_mode;
 
   // A knob to keep the data until next req, rather than driving unknown after handshake
   // completes. See #4465 for the detailed discussion
-  bit hold_h_data_until_next_req = 0;
-  bit hold_d_data_until_next_req = 0;
-
-  // Device-side delay range for both Push/Pull protocols.
-  rand int unsigned device_delay_min;
-  rand int unsigned device_delay_max;
-  rand int unsigned large_device_delay_max_weight = 10; // max 100
+  bit hold_h_data_until_next_req;
+  bit hold_d_data_until_next_req;
 
   // Host-side delay range for both Push/Pull protocols.
   rand int unsigned host_delay_min;
   rand int unsigned host_delay_max;
-  rand int unsigned large_host_delay_max_weight = 10; // max 100
+
+  // Device-side delay range for both Push/Pull protocols.
+  rand int unsigned device_delay_min;
+  rand int unsigned device_delay_max;
+
+  // 4-phase pull protocol delay ranges to de-assert req & ack.
+  rand int unsigned req_lo_delay_min;
+  rand int unsigned req_lo_delay_max;
+  rand int unsigned ack_lo_delay_min;
+  rand int unsigned ack_lo_delay_max;
 
   // Enables/disable all protocol delays.
   rand bit zero_delays;
@@ -51,22 +58,76 @@
   local bit [HostDataWidth-1:0]   h_user_data_q[$];
   local bit [DeviceDataWidth-1:0] d_user_data_q[$];
 
-  constraint device_delay_min_c {
-    device_delay_min == 0;
-  }
-
-  constraint device_delay_max_c {
-    device_delay_max dist {1000 :/ large_device_delay_max_weight,
-                           100  :/ 100 - large_device_delay_max_weight};
-  }
-
   constraint host_delay_min_c {
-    host_delay_min == 0;
+    soft host_delay_min == 0;
   }
 
   constraint host_delay_max_c {
-    host_delay_max dist {1000 :/ large_host_delay_max_weight,
-                         100  :/ 100 - large_host_delay_max_weight};
+    solve zero_delays before host_delay_max;
+    if (zero_delays) {
+      host_delay_max == 0;
+    } else {
+      host_delay_max dist {
+        [1:10] :/ 1,
+        [11:50] :/ 4,
+        [51:100] :/ 3,
+        [101:500] :/ 2,
+        [501:1000] :/ 1
+      };
+    }
+  }
+
+  constraint device_delay_min_c {
+    soft device_delay_min == 0;
+  }
+
+  constraint device_delay_max_c {
+    solve zero_delays before device_delay_max;
+    if (zero_delays) {
+      device_delay_max == 0;
+    } else {
+      device_delay_max dist {
+        [1:10] :/ 1,
+        [11:50] :/ 4,
+        [51:100] :/ 3,
+        [101:500] :/ 2,
+        [501:1000] :/ 1
+      };
+    }
+  }
+
+  constraint req_lo_delay_min_c {
+    soft req_lo_delay_min == 0;
+  }
+
+  constraint req_lo_delay_max_c {
+    solve zero_delays before req_lo_delay_max;
+    if (zero_delays) {
+      req_lo_delay_max == 0;
+    } else {
+      req_lo_delay_max dist {
+        [1:10] :/ 1,
+        [11:50] :/ 4,
+        [51:100] :/ 3
+      };
+    }
+  }
+
+  constraint ack_lo_delay_min_c {
+    soft ack_lo_delay_min == 0;
+  }
+
+  constraint ack_lo_delay_max_c {
+    solve zero_delays before ack_lo_delay_max;
+    if (zero_delays) {
+      ack_lo_delay_max == 0;
+    } else {
+      ack_lo_delay_max dist {
+        [1:10] :/ 1,
+        [11:50] :/ 4,
+        [51:100] :/ 3
+      };
+    }
   }
 
   // Bias randomization in favor of enabling zero delays less often.
@@ -124,16 +185,21 @@
   endfunction
 
   `uvm_object_param_utils_begin(push_pull_agent_cfg#(HostDataWidth, DeviceDataWidth))
-    `uvm_field_enum(push_pull_agent_e, agent_type, UVM_DEFAULT)
-    `uvm_field_int(in_bidirectional_mode,          UVM_DEFAULT)
-    `uvm_field_int(device_delay_min,               UVM_DEFAULT)
-    `uvm_field_int(device_delay_max,               UVM_DEFAULT)
-    `uvm_field_int(host_delay_min,                 UVM_DEFAULT)
-    `uvm_field_int(host_delay_max,                 UVM_DEFAULT)
-    `uvm_field_int(zero_delays,                    UVM_DEFAULT)
-    `uvm_field_int(start_default_device_seq,       UVM_DEFAULT)
-    `uvm_field_queue_int(h_user_data_q,            UVM_DEFAULT)
-    `uvm_field_queue_int(d_user_data_q,            UVM_DEFAULT)
+    `uvm_field_enum(push_pull_agent_e, agent_type,         UVM_DEFAULT)
+    `uvm_field_enum(pull_handshake_e, pull_handshake_type, UVM_DEFAULT)
+    `uvm_field_int(in_bidirectional_mode,                  UVM_DEFAULT)
+    `uvm_field_int(host_delay_min,                         UVM_DEFAULT)
+    `uvm_field_int(host_delay_max,                         UVM_DEFAULT)
+    `uvm_field_int(device_delay_min,                       UVM_DEFAULT)
+    `uvm_field_int(device_delay_max,                       UVM_DEFAULT)
+    `uvm_field_int(req_lo_delay_min,                       UVM_DEFAULT)
+    `uvm_field_int(req_lo_delay_max,                       UVM_DEFAULT)
+    `uvm_field_int(ack_lo_delay_min,                       UVM_DEFAULT)
+    `uvm_field_int(ack_lo_delay_max,                       UVM_DEFAULT)
+    `uvm_field_int(zero_delays,                            UVM_DEFAULT)
+    `uvm_field_int(start_default_device_seq,               UVM_DEFAULT)
+    `uvm_field_queue_int(h_user_data_q,                    UVM_DEFAULT)
+    `uvm_field_queue_int(d_user_data_q,                    UVM_DEFAULT)
   `uvm_object_utils_end
 
   `uvm_object_new
diff --git a/hw/dv/sv/push_pull_agent/push_pull_agent_pkg.sv b/hw/dv/sv/push_pull_agent/push_pull_agent_pkg.sv
index 031c44f..41b5fc8 100644
--- a/hw/dv/sv/push_pull_agent/push_pull_agent_pkg.sv
+++ b/hw/dv/sv/push_pull_agent/push_pull_agent_pkg.sv
@@ -18,6 +18,32 @@
     PullAgent
   } push_pull_agent_e;
 
+  // Pull req-ack handshake type.
+  typedef enum {
+    /*
+     * The two-phase handshake follows this protocol:
+     *             ____________________
+     * req _______/                    \____________
+     *                             ____
+     * ack _______________________/    \____________
+     *
+     * Ack asserts for 1 cycle. Req is accepted when both req and ack is true.
+     */
+    TwoPhase,
+
+    /*
+     * The four-phase handshake follows this protocol:
+     *             __________________
+     * req _______/                  \______________
+     *                   _______________________
+     * ack _____________/                       \___
+     *
+     * Req de-asserts after ack asserts. Ack de-asserts after the req
+     * de-asserts. This type of handshake is more common in async domains.
+     */
+    FourPhase
+  } pull_handshake_e;
+
   `include "push_pull_item.sv"
   `include "push_pull_agent_cfg.sv"
   `include "push_pull_agent_cov.sv"
diff --git a/hw/dv/sv/push_pull_agent/push_pull_driver_lib.sv b/hw/dv/sv/push_pull_agent/push_pull_driver_lib.sv
index 2a8319b..6cb84c8 100644
--- a/hw/dv/sv/push_pull_agent/push_pull_driver_lib.sv
+++ b/hw/dv/sv/push_pull_agent/push_pull_driver_lib.sv
@@ -52,24 +52,18 @@
   endfunction
 
   virtual task reset_signals();
-    // initial reset at start of test
     sub_driver.reset_signals();
     forever begin
       @(negedge cfg.vif.rst_n);
-      `uvm_info(`gfn, "Driver is resetting", UVM_HIGH)
-      sub_driver.set_in_reset(1'b1);
+      `uvm_info(`gfn, "Driver is under reset", UVM_HIGH)
       sub_driver.reset_signals();
       @(posedge cfg.vif.rst_n);
       `uvm_info(`gfn, "Driver is out of reset", UVM_HIGH)
-      sub_driver.set_in_reset(1'b0);
     end
   endtask
 
   // Drive trans received from sequencer.
   virtual task get_and_drive();
-    // Wait for the initial reset to pass.
-    @(posedge cfg.vif.rst_n);
-
     forever begin
       seq_item_port.try_next_item(req);
       if (req == null) begin
@@ -78,11 +72,7 @@
       end
 
       `uvm_info(`gfn, $sformatf("Driver received item:\n%0s", req.convert2string()), UVM_HIGH)
-      if (sub_driver.get_in_reset()) begin
-        `uvm_info(`gfn, "Item not applied to the DUT due to reset assertion", UVM_HIGH)
-      end else begin
-        sub_driver.drive_item(req);
-      end
+      sub_driver.drive_item(req);
       seq_item_port.item_done(req);
     end
   endtask
@@ -97,27 +87,11 @@
     parameter int DeviceDataWidth = HostDataWidth
 ) extends uvm_object;
 
-  // Indicates that the interface is in reset.
-  protected bit in_reset;
-
   // The handle to the agent cfg object set by the main driver.
   push_pull_agent_cfg #(HostDataWidth, DeviceDataWidth) cfg;
 
   `uvm_object_new
 
-  // Tell if the interface is in or out of reset.
-  //
-  // The main driver already implements a task that monitors the interface reset. The sub driver's
-  // in_reset bit is controlled by the main driver through this function.
-  virtual function void set_in_reset(bit in_reset);
-    this.in_reset = in_reset;
-  endfunction
-
-  // Returns the in_reset value.
-  virtual function bit get_in_reset();
-    return in_reset;
-  endfunction
-
   // Reset the interface signals driven to the DUT.
   pure virtual function void reset_signals();
 
@@ -153,14 +127,12 @@
   virtual task drive_item(push_pull_item#(HostDataWidth, DeviceDataWidth) req);
     `DV_SPINWAIT_EXIT(
         repeat (req.host_delay) @(`CB);
-        `CB.valid_int  <= 1'b1;
+        `CB.valid_int <= 1'b1;
         `CB.h_data_int <= req.h_data;
-        do begin
-          @(`CB);
-        end while (!`CB.ready);
+        do @(`CB); while (!`CB.ready);
         `CB.valid_int <= 1'b0;
         if (!cfg.hold_h_data_until_next_req) `CB.h_data_int <= 'x;,
-        wait(in_reset);)
+        wait (cfg.in_reset);)
   endtask
 
   `undef CB
@@ -191,14 +163,18 @@
   virtual task drive_item(push_pull_item#(HostDataWidth, DeviceDataWidth) req);
     `DV_SPINWAIT_EXIT(
         repeat (req.host_delay) @(`CB);
-        `CB.req_int    <= 1'b1;
+        `CB.req_int <= 1'b1;
         `CB.h_data_int <= req.h_data;
-        do begin
-          @(`CB);
-        end while (!`CB.ack);
-        `CB.req_int <= 1'b0;
+        do @(`CB); while (!`CB.ack);
+        if (cfg.pull_handshake_type == FourPhase) begin
+          repeat (req.req_lo_delay) @(`CB);
+          `CB.req_int <= 1'b0;
+          do @(`CB); while (`CB.ack);
+        end else begin
+          `CB.req_int <= 1'b0;
+        end
         if (!cfg.hold_h_data_until_next_req) `CB.h_data_int <= 'x;,
-        wait(in_reset);)
+        wait (cfg.in_reset);)
   endtask
 
   `undef CB
@@ -228,13 +204,14 @@
   // Drives device side of ready/valid protocol
   virtual task drive_item(push_pull_item#(HostDataWidth, DeviceDataWidth) req);
     `DV_SPINWAIT_EXIT(
+        // TODO: this may be needed in future: while (!`CB.valid) @(`CB);
         repeat (req.device_delay) @(`CB);
-        `CB.ready_int  <= 1'b1;
+        `CB.ready_int <= 1'b1;
         `CB.d_data_int <= req.d_data;
         @(`CB);
-        `CB.ready_int  <= 1'b0;
+        `CB.ready_int <= 1'b0;
         if (!cfg.hold_d_data_until_next_req) `CB.d_data_int <= 'x;,
-        wait(in_reset);)
+        wait (cfg.in_reset);)
   endtask
 
   `undef CB
@@ -266,12 +243,17 @@
     `DV_SPINWAIT_EXIT(
         while (!`CB.req) @(`CB);
         repeat (req.device_delay) @(`CB);
-        `CB.ack_int    <= 1'b1;
+        `CB.ack_int <= 1'b1;
         `CB.d_data_int <= req.d_data;
-        @(`CB);
-        `CB.ack_int    <= 1'b0;
+        if (cfg.pull_handshake_type == FourPhase) begin
+          do @(`CB); while (`CB.req);
+          repeat (req.ack_lo_delay) @(`CB);
+        end else begin
+          @(`CB);
+        end
+        `CB.ack_int <= 1'b0;
         if (!cfg.hold_d_data_until_next_req) `CB.d_data_int <= 'x;,
-        wait(in_reset);)
+        wait (cfg.in_reset);)
   endtask
 
   `undef CB
diff --git a/hw/dv/sv/push_pull_agent/push_pull_if.sv b/hw/dv/sv/push_pull_agent/push_pull_if.sv
index cdbd9a5..49c8c49 100644
--- a/hw/dv/sv/push_pull_agent/push_pull_if.sv
+++ b/hw/dv/sv/push_pull_agent/push_pull_if.sv
@@ -47,6 +47,9 @@
   // transferring data on both sides of the handshake.
   bit in_bidirectional_mode;
 
+  // Indicates whether pull interface follows 2-phase (0) or 4-phase (1) handshake.
+  bit is_pull_handshake_4_phase;
+
   // clocking blocks
   clocking host_push_cb @(posedge clk);
     input   ready;
@@ -140,7 +143,7 @@
   // When req is asserted but ack is low, and the agent is in bidirectional mode,
   // h_data must remain stable.
   `ASSERT_IF(H_DataStableWhenBidirectionalAndReq_A, (req && !ack) |=> $stable(h_data),
-             in_bidirectional_mode, clk, !rst_n)
+             !is_push_agent && in_bidirectional_mode, clk, !rst_n)
 
   // TODO: The following two assertions make a rather important assumption about the req/ack
   //       protocol that will be used for the key/csrng interfaces, which is that no requests
@@ -150,11 +153,23 @@
   //       Based on the final decision on this issue, these assertions may have to be removed
   //       if it is allowed for requests to be dropped.
 
-  // ack cannot be 1 if req is not 1.
-  `ASSERT_IF(AckAssertedOnlyWhenReqAsserted_A, ack |-> req, !is_push_agent, clk, !rst_n)
+  // I 2-phase req-ack handshake, ack cannot be 1 if req is not 1.
+  `ASSERT_IF(AckAssertedOnlyWhenReqAsserted_A, ack |-> req,
+             !is_push_agent && !is_pull_handshake_4_phase, clk, !rst_n)
+
+  // Req is asserted only after previous ack is de-asserted.
+  `ASSERT_IF(NoAckOnNewReq_A, $rose(req) |-> !($past(ack, 1)) && !ack,
+             !is_push_agent, clk, !rst_n)
 
   // When req is asserted, it must stay high until a corresponding ack is seen.
-  `ASSERT_IF(ReqHighUntilAck_A, $rose(req) |-> (req throughout ack [->1]),
+  `ASSERT_IF(ReqHighUntilAck_A, $rose(req) |-> (req throughout ack[->1]),
              !is_push_agent, clk, !rst_n)
 
+  // When ack is asserted, it must stay high until a corresponding req is de-asserted,
+  // in case of four-phase handshake.
+  `ASSERT_IF(AckHighUntilReq_A, $rose(ack) |-> (ack throughout (!req[->1])),
+             !is_push_agent && is_pull_handshake_4_phase, clk, !rst_n)
+
+  // TODO: Add support for async clock domains.
+
 endinterface
diff --git a/hw/dv/sv/push_pull_agent/push_pull_item.sv b/hw/dv/sv/push_pull_agent/push_pull_item.sv
index 84a0a9b..7a80659 100644
--- a/hw/dv/sv/push_pull_agent/push_pull_item.sv
+++ b/hw/dv/sv/push_pull_agent/push_pull_item.sv
@@ -9,17 +9,37 @@
   rand bit [HostDataWidth-1:0]      h_data;
   rand bit [DeviceDataWidth-1:0]    d_data;
 
-  // Host-side delay for both Push/Pull protocols.
+  // Host-side delay for both push/pull protocols.
   rand int unsigned host_delay;
+  constraint host_delay_max_c {
+    soft host_delay <= 1000;
+  }
 
-  // Device-side delay for both Push/Pull protocols.
+  // Device-side delay for both push/pull protocols.
   rand int unsigned device_delay;
+  constraint device_delay_max_c  {
+    soft device_delay <= 1000;
+  }
+
+  // 4-phase pull protocol delays to de-assert req. This delay is applied after ack assertion.
+  rand int unsigned req_lo_delay;
+  constraint req_lo_delay_max_c {
+    soft req_lo_delay <= 1000;
+  }
+
+  // 4-phase pull protocol delays to de-assert ack. This delay is applied after req de-assertion.
+  rand int unsigned ack_lo_delay;
+  constraint ack_lo_delay_max_c {
+    soft ack_lo_delay <= 1000;
+  }
 
   `uvm_object_param_utils_begin(push_pull_item#(HostDataWidth, DeviceDataWidth))
     `uvm_field_int(h_data,     UVM_DEFAULT)
     `uvm_field_int(d_data,     UVM_DEFAULT)
     `uvm_field_int(host_delay,   UVM_DEFAULT)
     `uvm_field_int(device_delay, UVM_DEFAULT)
+    `uvm_field_int(req_lo_delay, UVM_DEFAULT)
+    `uvm_field_int(ack_lo_delay, UVM_DEFAULT)
   `uvm_object_utils_end
 
   `uvm_object_new
@@ -27,8 +47,10 @@
   virtual function string convert2string();
     return {$sformatf("h_data = 0x%0x ", h_data),
             $sformatf("d_data = 0x%0x ", d_data),
-            $sformatf("host_delay = 0x%0x ", host_delay),
-            $sformatf("device_delay = 0x%0x ", device_delay)};
+            $sformatf("host_delay = 0x%0d ", host_delay),
+            $sformatf("device_delay = 0x%0d ", device_delay),
+            $sformatf("req_lo_delay = 0x%0d ", req_lo_delay),
+            $sformatf("ack_lo_delay = 0x%0d ", ack_lo_delay)};
   endfunction
 
 endclass
diff --git a/hw/dv/sv/push_pull_agent/push_pull_monitor.sv b/hw/dv/sv/push_pull_agent/push_pull_monitor.sv
index db10a28..70ab9ad 100644
--- a/hw/dv/sv/push_pull_agent/push_pull_monitor.sv
+++ b/hw/dv/sv/push_pull_agent/push_pull_monitor.sv
@@ -23,12 +23,9 @@
     @(posedge cfg.vif.rst_n);
     fork
       monitor_reset();
-      collect_valid_trans();
-      // We only need to monitor incoming requests if the agent is configured
-      // in device mode and is using Pull protocol.
-      if (cfg.if_mode == dv_utils_pkg::Device && cfg.agent_type == PullAgent) begin
-        collect_request();
-      end
+      collect_trans(phase);
+      // Collect partial pull reqs for the reactive pull device agent.
+      collect_pull_req();
     join_none
   endtask
 
@@ -36,30 +33,83 @@
     forever begin
       @(negedge cfg.vif.rst_n);
       cfg.in_reset = 1;
-      // TODO: sample any reset-related covergroups
       @(posedge cfg.vif.rst_n);
       cfg.in_reset = 0;
     end
   endtask
 
+  // Shorthand for restarting forever loop on reset detection.
+  `define WAIT_FOR_RESET    \
+    if (cfg.in_reset) begin \
+      wait (!cfg.in_reset); \
+      continue;             \
+    end
+
+  // Collect fully-completed transactions.
+  //
   // TODO : sample covergroups
-  virtual protected task collect_valid_trans();
-    forever begin
-      @(cfg.vif.mon_cb);
-      if (cfg.agent_type == PushAgent) begin
+  virtual protected task collect_trans(uvm_phase phase);
+    if (cfg.agent_type == PushAgent) begin
+      forever begin
+        @(cfg.vif.mon_cb);
+        `WAIT_FOR_RESET
         if (cfg.vif.mon_cb.ready && cfg.vif.mon_cb.valid) begin
           create_and_write_item();
-          // TODO: sample covergroups
         end
-      end else begin
+      end
+    end else begin
+      forever begin
+        @(cfg.vif.mon_cb);
+        `WAIT_FOR_RESET
         if (cfg.vif.mon_cb.req && cfg.vif.mon_cb.ack) begin
           create_and_write_item();
-          // TODO: sample covergroups
+          // Wait for req to de-assert in case of four-phase handshake.
+          if (cfg.pull_handshake_type == FourPhase) begin
+            `uvm_info(`gfn, "Waiting for 4-phase req-ack to de-asssert", UVM_HIGH)
+            `DV_SPINWAIT_EXIT(while (cfg.vif.mon_cb.ack) @(cfg.vif.mon_cb);,
+                              wait (cfg.in_reset))
+          end
         end
       end
     end
   endtask
 
+  // Collects partial pull requests.
+  //
+  // This task is only used for device agents using the Pull protocol.
+  // It will pick up any incoming requests from the DUT and send a signal to the
+  // sequencer (in the form of a sequence item), which will then be forwarded to
+  // the sequence, which then generates the appropriate response item.
+  //
+  // TODO: This assumes requests cannot be dropped, and might need to be fixed
+  //       if this is allowed.
+  virtual protected task collect_pull_req();
+    push_pull_item#(HostDataWidth, DeviceDataWidth) item;
+
+    if (!(cfg.agent_type == PullAgent && cfg.if_mode == dv_utils_pkg::Device)) return;
+    forever begin
+      @(cfg.vif.mon_cb);
+      `WAIT_FOR_RESET
+      if (cfg.vif.mon_cb.req) begin
+        `uvm_info(`gfn, $sformatf("[%0s] pull req detected", cfg.agent_type), UVM_HIGH)
+        // TODO: sample any covergroups
+        item = push_pull_item#(HostDataWidth, DeviceDataWidth)::type_id::create("item");
+        item.h_data = cfg.vif.mon_cb.h_data;
+        req_analysis_port.write(item);
+        // After picking up a request, wait until a response is sent before
+        // detecting another request, as this is not a pipelined protocol.
+        `DV_SPINWAIT_EXIT(while (!cfg.vif.mon_cb.ack) @(cfg.vif.mon_cb);,
+                          wait (cfg.in_reset))
+        if (cfg.pull_handshake_type == FourPhase) begin
+          `DV_SPINWAIT_EXIT(while (cfg.vif.mon_cb.ack) @(cfg.vif.mon_cb);,
+                            wait (cfg.in_reset))
+        end
+      end
+    end
+  endtask
+
+  `undef WAIT_FOR_RESET
+
   // Creates and writes the item to the analysis_port.
   //
   // The onus is on the caller to invoke this function at the right time -
@@ -75,39 +125,17 @@
     analysis_port.write(item);
   endfunction
 
-  // This task is only used for device agents using the Pull protocol.
-  // It will pick up any incoming requests from the DUT and send a signal to the
-  // sequencer (in the form of a sequence item), which will then be forwarded to
-  // the sequence, which then generates the appropriate response item.
+  // Detects periods of inactivity for the ok_to_end watchdog.
   //
-  // TODO: This assumes no requests can be dropped, and might need to be fixed
-  //       if this is not allowed.
-  virtual protected task collect_request();
-    push_pull_item#(HostDataWidth, DeviceDataWidth) item;
-    forever begin
-      @(cfg.vif.mon_cb);
-      if (cfg.vif.req) begin
-        `uvm_info(`gfn, "detected pull request", UVM_HIGH)
-        // TODO: sample any covergroups
-        item = push_pull_item#(HostDataWidth, DeviceDataWidth)::type_id::create("item");
-        req_analysis_port.write(item);
-        // After picking up a request, wait until a response is sent before
-        // detecting another request, as this is not a pipelined protocol.
-        `DV_SPINWAIT_EXIT(while (!cfg.vif.mon_cb.ack) @(cfg.vif.mon_cb);,
-                          wait(cfg.in_reset))
-       end
-    end
-  endtask
-
-  // update ok_to_end to prevent simulation from finishing when
-  // there is any activity on the bus.
+  // Set ok_to_end bit to detect inactivity on the bus. Spawned by
+  // dv_base_monitor as a thread towards the end of run_phase.
   virtual task monitor_ready_to_end();
     forever begin
       @(cfg.vif.mon_cb);
       if (cfg.agent_type == PushAgent) begin
-        ok_to_end = (cfg.vif.valid == 0);
+        ok_to_end = !cfg.vif.mon_cb.valid;
       end else begin
-        ok_to_end = (cfg.vif.req == 0) && (cfg.vif.ack == 0);
+        ok_to_end = !cfg.vif.mon_cb.req && !cfg.vif.mon_cb.ack;
       end
     end
   endtask
diff --git a/hw/dv/sv/push_pull_agent/seq_lib/push_pull_base_seq.sv b/hw/dv/sv/push_pull_agent/seq_lib/push_pull_base_seq.sv
index 8de6a0a..66b858a 100644
--- a/hw/dv/sv/push_pull_agent/seq_lib/push_pull_base_seq.sv
+++ b/hw/dv/sv/push_pull_agent/seq_lib/push_pull_base_seq.sv
@@ -13,6 +13,25 @@
 
   `uvm_object_new
 
+  // Randomizes the req or response.
+  //
+  // Regardless of agent or item type, we can apply the same set of limits.
+  virtual function void randomize_item(push_pull_item #(HostDataWidth, DeviceDataWidth) item);
+    `DV_CHECK_RANDOMIZE_WITH_FATAL(item,
+      if (cfg.zero_delays) {
+        host_delay == 0;
+        device_delay == 0;
+        req_lo_delay == 0;
+        ack_lo_delay == 0;
+      } else {
+        host_delay inside {[cfg.host_delay_min : cfg.host_delay_max]};
+        device_delay inside {[cfg.device_delay_min : cfg.device_delay_max]};
+        req_lo_delay inside {[cfg.req_lo_delay_min : cfg.req_lo_delay_max]};
+        ack_lo_delay inside {[cfg.ack_lo_delay_min : cfg.ack_lo_delay_max]};
+      }
+    )
+  endfunction
+
   virtual task body();
     `uvm_fatal(`gtn, "Need to override this when you extend from this class!")
   endtask
diff --git a/hw/dv/sv/push_pull_agent/seq_lib/push_pull_device_seq.sv b/hw/dv/sv/push_pull_agent/seq_lib/push_pull_device_seq.sv
index 00a09f2..0b15f3f 100644
--- a/hw/dv/sv/push_pull_agent/seq_lib/push_pull_device_seq.sv
+++ b/hw/dv/sv/push_pull_agent/seq_lib/push_pull_device_seq.sv
@@ -14,62 +14,62 @@
 
   `uvm_object_new
 
-  mailbox #(push_pull_item#(HostDataWidth, DeviceDataWidth)) req_mailbox;
+  // Randomizes the device rsp.
+  //
+  // The randomization works out the same regardless of the agent type.
+  virtual function void randomize_item(push_pull_item #(HostDataWidth, DeviceDataWidth) item);
+    super.randomize_item(item);
+    // If user-provided data is available, use it.
+    if (cfg.has_d_user_data()) item.d_data = cfg.get_d_user_data();
+  endfunction
 
   virtual task body();
-    req_mailbox = new();
-
     if (cfg.agent_type == PushAgent) begin
-      // In Push mode, continuously send an empty but randomized sequence item
-      // to the device driver (which just needs to assert ready).
-      // If the agent is in bidirectional mode, send the corresponding data.
-      forever begin
-        // Pause if we are in reset.
-        if (cfg.in_reset) begin
-          wait(!cfg.in_reset);
-        end
-
-        req = push_pull_item#(HostDataWidth, DeviceDataWidth)::type_id::create("req");
-        start_item(req);
-        `DV_CHECK_RANDOMIZE_WITH_FATAL(req,
-          if (cfg.zero_delays) {
-            device_delay == 0;
-          } else {
-            device_delay inside {[cfg.device_delay_min : cfg.device_delay_max]};
-          }
-        )
-        // If user-provided data is available, use this instead of randomized data.
-        if (cfg.has_d_user_data()) req.d_data = cfg.get_d_user_data();
-        finish_item(req);
-        get_response(rsp);
-        `uvm_info(`gfn, $sformatf("Received response: %0s", rsp.convert2string()), UVM_HIGH)
-      end
+      push_device_thread();
     end else begin
-      fork
-        forever begin : collect_req
-          // We indefinitely poll for any traffic sent from the monitor and store it to a mailbox.
-          p_sequencer.req_analysis_fifo.get(req);
-          req_mailbox.put(req);
-        end : collect_req
-        forever begin : send_req
-          // Collect transactions stored in the mailbox and send them to the driver.
-          req_mailbox.get(rsp);
-          start_item(rsp);
-          `DV_CHECK_RANDOMIZE_WITH_FATAL(rsp,
-            if (cfg.zero_delays) {
-              device_delay == 0;
-            } else {
-              device_delay inside {[cfg.device_delay_min : cfg.device_delay_max]};
-            }
-          )
-          // If user-provided data is availabe, use this instead of randomized data.
-          if (cfg.has_d_user_data()) rsp.d_data = cfg.get_d_user_data();
-          finish_item(rsp);
-          get_response(rsp);
-          `uvm_info(`gfn, $sformatf("Received response: %0s", rsp.convert2string()), UVM_HIGH)
-        end : send_req
-      join
+      pull_device_thread();
     end
   endtask
 
+  // Sends random rsps (assertion of ready) back to host.
+  //
+  // In Push mode, continuously send an empty but randomized sequence item
+  // to the device driver (which just needs to assert ready).
+  // If the agent is in bidirectional mode, send the corresponding data.
+  // TODO: for bidirectional mode, there may be a need for the returned device data to be
+  // constructed based on the received host data. This may need to be made "reactive" as well.
+  virtual task push_device_thread();
+    forever begin
+      wait (!cfg.in_reset);
+      `uvm_create(req)
+      start_item(req);
+      randomize_item(req);
+      finish_item(req);
+      get_response(rsp);
+    end
+  endtask
+
+  // Sends random rsps (device data and ack) back to host.
+  virtual task pull_device_thread();
+    push_pull_item #(HostDataWidth, DeviceDataWidth) req_q[$];
+    fork
+      forever begin : get_req
+        p_sequencer.req_analysis_fifo.get(req);
+        req_q.push_back(req);
+      end : get_req
+      forever begin : send_rsp
+        if (cfg.in_reset) begin
+          req_q.delete();
+          wait (!cfg.in_reset);
+        end
+        wait (req_q.size());
+        rsp = req_q.pop_front();
+        start_item(rsp);
+        randomize_item(rsp);
+        finish_item(rsp);
+        get_response(rsp);
+      end : send_rsp
+    join
+  endtask
+
 endclass
diff --git a/hw/dv/sv/push_pull_agent/seq_lib/push_pull_host_seq.sv b/hw/dv/sv/push_pull_agent/seq_lib/push_pull_host_seq.sv
index e562213..98c3b02 100644
--- a/hw/dv/sv/push_pull_agent/seq_lib/push_pull_host_seq.sv
+++ b/hw/dv/sv/push_pull_agent/seq_lib/push_pull_host_seq.sv
@@ -17,25 +17,20 @@
   // Can be overridden at a higher layer.
   int unsigned num_trans = 1;
 
+  // Randomizes the host req.
+  virtual function void randomize_item(push_pull_item #(HostDataWidth, DeviceDataWidth) item);
+    super.randomize_item(item);
+    // If user-provided data is available, use it.
+    if (cfg.has_h_user_data()) item.h_data = cfg.get_h_user_data();
+  endfunction
+
   virtual task body();
-    for (int i = 0; i < num_trans; i++) begin : send_req
-      req = push_pull_item#(HostDataWidth, DeviceDataWidth)::type_id::create(
-        $sformatf("req[%0d]", i));
+    repeat (num_trans) begin : send_req
+      `uvm_create(req)
       start_item(req);
-      `DV_CHECK_RANDOMIZE_WITH_FATAL(req,
-        if (cfg.zero_delays) {
-          host_delay == 0;
-        } else {
-          host_delay inside {[cfg.host_delay_min : cfg.host_delay_max]};
-        }
-      )
-      // If user-provided data is ready, use this instead of random data.
-      if (cfg.has_h_user_data()) req.h_data = cfg.get_h_user_data();
-      `uvm_info(`gfn, $sformatf("Starting sequence item: %0s", req.convert2string()), UVM_HIGH)
-      // We don't care about response data here, the monitor will log all complete transactions.
+      randomize_item(req);
       finish_item(req);
       get_response(rsp);
-      `uvm_info(`gfn, $sformatf("Received response: %0s", rsp.convert2string()), UVM_HIGH)
     end : send_req
   endtask