Refactor ProtocolAdapter to return a Model result with a schema version.

PiperOrigin-RevId: 251251773
Change-Id: I9b6a9f6c9b085859a466c01929322da265ac34f3
diff --git a/src/main/java/com/google/android/libraries/feed/api/internal/common/Model.java b/src/main/java/com/google/android/libraries/feed/api/internal/common/Model.java
new file mode 100644
index 0000000..f6e3d14
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/api/internal/common/Model.java
@@ -0,0 +1,43 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed 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.
+
+package com.google.android.libraries.feed.api.internal.common;
+
+import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
+import java.util.Collections;
+import java.util.List;
+
+/** Contains a list of {@link StreamDataOperations}s and a schema version. */
+public final class Model {
+  /** The current schema version. */
+  public static final int CURRENT_SCHEMA_VERSION = 1;
+
+  public final List<StreamDataOperation> streamDataOperations;
+  public final int schemaVersion;
+
+  private Model(List<StreamDataOperation> streamDataOperations, int schemaVersion) {
+    this.streamDataOperations =
+        Collections.unmodifiableList(
+            Collections.list(Collections.enumeration(streamDataOperations)));
+    this.schemaVersion = schemaVersion;
+  }
+
+  public static Model of(List<StreamDataOperation> streamDataOperations) {
+    return new Model(streamDataOperations, CURRENT_SCHEMA_VERSION);
+  }
+
+  public static Model empty() {
+    return new Model(Collections.emptyList(), CURRENT_SCHEMA_VERSION);
+  }
+}
diff --git a/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/BUILD b/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/BUILD
index b6196a4..0958d38 100644
--- a/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/BUILD
@@ -6,6 +6,7 @@
     name = "protocoladapter",
     srcs = glob(["*.java"]),
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
         "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
diff --git a/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/ProtocolAdapter.java b/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/ProtocolAdapter.java
index 346789c..d6c3eaa 100644
--- a/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/ProtocolAdapter.java
+++ b/src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter/ProtocolAdapter.java
@@ -14,6 +14,7 @@
 
 package com.google.android.libraries.feed.api.internal.protocoladapter;
 
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.common.Result;
 import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
@@ -25,9 +26,10 @@
 public interface ProtocolAdapter {
   /**
    * Create the internal protocol from a wire protocol response definition. The wire protocol is
-   * turned into a List of {@link StreamDataOperation} which are sent to the SessionManager.
+   * turned into a {@link Model} containing a list of {@link StreamDataOperation}s and a schema
+   * version.
    */
-  Result<List<StreamDataOperation>> createModel(Response response);
+  Result<Model> createModel(Response response);
 
   /**
    * Create {@link StreamDataOperation}s from the internal protocol for the wire protocol
diff --git a/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/BUILD b/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/BUILD
index 26807c6..3349377 100644
--- a/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/BUILD
@@ -7,6 +7,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//src/main/java/com/google/android/libraries/feed/api/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
diff --git a/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/FeedRequestManager.java b/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/FeedRequestManager.java
index 68cc431..84bca33 100644
--- a/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/FeedRequestManager.java
+++ b/src/main/java/com/google/android/libraries/feed/api/internal/requestmanager/FeedRequestManager.java
@@ -15,12 +15,11 @@
 package com.google.android.libraries.feed.api.internal.requestmanager;
 
 import com.google.android.libraries.feed.api.host.logging.RequestReason;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.common.Result;
 import com.google.android.libraries.feed.common.functional.Consumer;
-import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
 import com.google.search.now.feed.client.StreamDataProto.StreamToken;
 import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
-import java.util.List;
 
 /**
  * Creates and issues requests to the server.
@@ -32,33 +31,27 @@
   /**
    * Issues a request for the next page of data. The {@code streamToken} described to the server
    * what the next page means. The{@code token} is used by the server for consistent data across
-   * requests. The response will be sent to a {@link Consumer} a set of {@link StreamDataOperation}
-   * created by the ProtocolAdapter.
+   * requests. The response will be sent to a {@link Consumer} with a {@link Model} created by the
+   * ProtocolAdapter.
    */
-  void loadMore(
-      StreamToken streamToken,
-      ConsistencyToken token,
-      Consumer<Result<List<StreamDataOperation>>> consumer);
+  void loadMore(StreamToken streamToken, ConsistencyToken token, Consumer<Result<Model>> consumer);
 
   /**
    * Issues a request to refresh the entire feed, with the consumer being called back with the
-   * resulting {@link StreamDataOperation}s.
+   * resulting {@link Model}.
    *
    * @param reason The reason for this refresh.
    */
-  void triggerRefresh(
-      @RequestReason int reason, Consumer<Result<List<StreamDataOperation>>> consumer);
+  void triggerRefresh(@RequestReason int reason, Consumer<Result<Model>> consumer);
 
   /**
    * Issues a request to refresh the entire feed, with the consumer being called back with the
-   * resulting {@link StreamDataOperation}s.
+   * resulting {@link Model}.
    *
    * @param reason The reason for this refresh.
    * @param token Used by the server for consistent data across requests.
    * @param consumer The consumer called after the refresh is performed.
    */
   void triggerRefresh(
-      @RequestReason int reason,
-      ConsistencyToken token,
-      Consumer<Result<List<StreamDataOperation>>> consumer);
+      @RequestReason int reason, ConsistencyToken token, Consumer<Result<Model>> consumer);
 }
diff --git a/src/main/java/com/google/android/libraries/feed/api/internal/sessionmanager/FeedSessionManager.java b/src/main/java/com/google/android/libraries/feed/api/internal/sessionmanager/FeedSessionManager.java
index 8bbc7ad..982d624 100644
--- a/src/main/java/com/google/android/libraries/feed/api/internal/sessionmanager/FeedSessionManager.java
+++ b/src/main/java/com/google/android/libraries/feed/api/internal/sessionmanager/FeedSessionManager.java
@@ -17,6 +17,7 @@
 import com.google.android.libraries.feed.api.client.knowncontent.KnownContent;
 import com.google.android.libraries.feed.api.common.MutationContext;
 import com.google.android.libraries.feed.api.host.logging.RequestReason;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
 import com.google.android.libraries.feed.api.internal.lifecycle.Resettable;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider;
@@ -134,8 +135,8 @@
 
   /**
    * Return a {@link Consumer} which is able to handle an update to the SessionManager. An update
-   * consists of a List of {@link StreamDataOperation} objects. The {@link MutationContext} captures
-   * the context which is initiating the update operation.
+   * consists of {@link Model} containing a list of {@link StreamDataOperation} objects. The {@link
+   * MutationContext} captures the context which is initiating the update operation.
    */
-  Consumer<Result<List<StreamDataOperation>>> getUpdateConsumer(MutationContext mutationContext);
+  Consumer<Result<Model>> getUpdateConsumer(MutationContext mutationContext);
 }
diff --git a/src/main/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImpl.java b/src/main/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImpl.java
index 1756990..947a0d4 100644
--- a/src/main/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImpl.java
+++ b/src/main/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImpl.java
@@ -19,6 +19,7 @@
 import com.google.android.libraries.feed.api.common.MutationContext;
 import com.google.android.libraries.feed.api.host.logging.Task;
 import com.google.android.libraries.feed.api.internal.actionmanager.ActionManager;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
 import com.google.android.libraries.feed.api.internal.sessionmanager.FeedSessionManager;
 import com.google.android.libraries.feed.api.internal.store.LocalActionMutation;
@@ -150,6 +151,6 @@
     }
     feedSessionManager
         .getUpdateConsumer(mutationContextBuilder.build())
-        .accept(Result.success(streamDataOperations));
+        .accept(Result.success(Model.of(streamDataOperations)));
   }
 }
diff --git a/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD b/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD
index 476d6fe..55d52d7 100644
--- a/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD
@@ -6,6 +6,7 @@
     name = "feedprotocoladapter",
     srcs = glob(["*.java"]),
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/logging",
diff --git a/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapter.java b/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapter.java
index 2288b19..5f45e79 100644
--- a/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapter.java
+++ b/src/main/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapter.java
@@ -14,6 +14,7 @@
 
 package com.google.android.libraries.feed.feedprotocoladapter;
 
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.api.internal.protocoladapter.RequiredContentAdapter;
 import com.google.android.libraries.feed.common.Result;
@@ -107,13 +108,19 @@
   }
 
   @Override
-  public Result<List<StreamDataOperation>> createModel(Response response) {
+  public Result<Model> createModel(Response response) {
     responseHandlingCount++;
 
     FeedResponse feedResponse = response.getExtension(FeedResponse.feedResponse);
     Logger.i(TAG, "createModel, operations %s", feedResponse.getDataOperationCount());
-    return createOperations(
-        feedResponse.getDataOperationList(), feedResponse.getFeedResponseMetadata());
+    Result<List<StreamDataOperation>> result =
+        createOperations(
+            feedResponse.getDataOperationList(), feedResponse.getFeedResponseMetadata());
+    if (result.isSuccessful()) {
+      return Result.success(Model.of(result.getValue()));
+    } else {
+      return Result.failure();
+    }
   }
 
   @Override
diff --git a/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImpl.java b/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImpl.java
index 9a20772..89bc495 100644
--- a/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImpl.java
+++ b/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImpl.java
@@ -31,6 +31,7 @@
 import com.google.android.libraries.feed.api.host.stream.TooltipSupportedApi;
 import com.google.android.libraries.feed.api.internal.actionmanager.ActionReader;
 import com.google.android.libraries.feed.api.internal.common.DismissActionWithSemanticProperties;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.api.internal.requestmanager.FeedRequestManager;
@@ -47,7 +48,6 @@
 import com.google.android.libraries.feed.common.time.TimingUtils.ElapsedTimeTracker;
 import com.google.android.libraries.feed.feedrequestmanager.internal.Utils;
 import com.google.protobuf.ByteString;
-import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
 import com.google.search.now.feed.client.StreamDataProto.StreamToken;
 import com.google.search.now.wire.feed.ActionTypeProto;
 import com.google.search.now.wire.feed.CapabilityProto.Capability;
@@ -128,9 +128,7 @@
 
   @Override
   public void loadMore(
-      StreamToken streamToken,
-      ConsistencyToken token,
-      Consumer<Result<List<StreamDataOperation>>> consumer) {
+      StreamToken streamToken, ConsistencyToken token, Consumer<Result<Model>> consumer) {
     threadUtils.checkNotMainThread();
 
     Logger.i(TAG, "Task: FeedRequestManagerImpl LoadMore");
@@ -145,16 +143,13 @@
   }
 
   @Override
-  public void triggerRefresh(
-      @RequestReason int reason, Consumer<Result<List<StreamDataOperation>>> consumer) {
+  public void triggerRefresh(@RequestReason int reason, Consumer<Result<Model>> consumer) {
     triggerRefresh(reason, ConsistencyToken.getDefaultInstance(), consumer);
   }
 
   @Override
   public void triggerRefresh(
-      @RequestReason int reason,
-      ConsistencyToken token,
-      Consumer<Result<List<StreamDataOperation>>> consumer) {
+      @RequestReason int reason, ConsistencyToken token, Consumer<Result<Model>> consumer) {
     Logger.i(TAG, "trigger refresh %s", reason);
     RequestBuilder request = newDefaultRequest(reason).setConsistencyToken(token);
 
@@ -199,8 +194,7 @@
     }
   }
 
-  private void executeRequest(
-      RequestBuilder requestBuilder, Consumer<Result<List<StreamDataOperation>>> consumer) {
+  private void executeRequest(RequestBuilder requestBuilder, Consumer<Result<Model>> consumer) {
     threadUtils.checkNotMainThread();
     Result<List<DismissActionWithSemanticProperties>> dismissActionsResult =
         actionReader.getDismissActionsWithSemanticProperties();
@@ -233,8 +227,7 @@
     }
   }
 
-  private void sendRequest(
-      RequestBuilder requestBuilder, Consumer<Result<List<StreamDataOperation>>> consumer) {
+  private void sendRequest(RequestBuilder requestBuilder, Consumer<Result<Model>> consumer) {
     threadUtils.checkNotMainThread();
     String endpoint = configuration.getValueOrDefault(ConfigKey.FEED_SERVER_ENDPOINT, "");
     @HttpMethod
@@ -278,7 +271,7 @@
   }
 
   private void handleResponseBytes(
-      final byte[] responseBytes, final Consumer<Result<List<StreamDataOperation>>> consumer) {
+      final byte[] responseBytes, final Consumer<Result<Model>> consumer) {
     taskQueue.execute(
         Task.HANDLE_RESPONSE_BYTES,
         TaskType.IMMEDIATE,
@@ -298,9 +291,9 @@
             consumer.accept(Result.failure());
             return;
           }
-          Result<List<StreamDataOperation>> result = protocolAdapter.createModel(response);
           mainThreadRunner.execute(
-              "FeedRequestManagerImpl consumer", () -> consumer.accept(result));
+              "FeedRequestManagerImpl consumer",
+              () -> consumer.accept(protocolAdapter.createModel(response)));
         });
   }
 
diff --git a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java
index 0705c50..0aa4956 100644
--- a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java
+++ b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImpl.java
@@ -25,6 +25,7 @@
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi.RequestBehavior;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi.SessionState;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
 import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelError;
@@ -660,8 +661,7 @@
   }
 
   @Override
-  public Consumer<Result<List<StreamDataOperation>>> getUpdateConsumer(
-      MutationContext mutationContext) {
+  public Consumer<Result<Model>> getUpdateConsumer(MutationContext mutationContext) {
     if (!initialized.get()) {
       Logger.i(TAG, "Lazy initialization triggered, getUpdateConsumer");
       initialize();
@@ -670,7 +670,7 @@
   }
 
   @VisibleForTesting
-  class SessionMutationTracker implements Consumer<Result<List<StreamDataOperation>>> {
+  class SessionMutationTracker implements Consumer<Result<Model>> {
     private final MutationContext mutationContext;
     private final String taskName;
 
@@ -682,10 +682,10 @@
     }
 
     @Override
-    public void accept(Result<List<StreamDataOperation>> input) {
+    public void accept(Result<Model> input) {
       if (outstandingMutations.remove(this)) {
         if (input.isSuccessful()) {
-          updateSharedStateCache(input.getValue());
+          updateSharedStateCache(input.getValue().streamDataOperations);
         }
         sessionManagerMutation
             .createCommitter(
@@ -736,8 +736,7 @@
     reset();
   }
 
-  private Consumer<Result<List<StreamDataOperation>>> getCommitter(
-      String taskName, MutationContext mutationContext) {
+  private Consumer<Result<Model>> getCommitter(String taskName, MutationContext mutationContext) {
     return new SessionMutationTracker(mutationContext, taskName);
   }
 
diff --git a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java
index 52ca387..cc61d23 100644
--- a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java
+++ b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutation.java
@@ -21,6 +21,7 @@
 import com.google.android.libraries.feed.api.host.logging.Task;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelError;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelError.ErrorType;
@@ -105,7 +106,7 @@
    * Return a Consumer of StreamDataOperations which will update the {@link
    * com.google.android.libraries.feed.api.internal.sessionmanager.FeedSessionManager}.
    */
-  public Consumer<Result<List<StreamDataOperation>>> createCommitter(
+  public Consumer<Result<Model>> createCommitter(
       String task,
       MutationContext mutationContext,
       ModelErrorObserver modelErrorObserver,
@@ -214,8 +215,7 @@
   }
 
   @VisibleForTesting
-  class MutationCommitter extends HeadMutationCommitter
-      implements Consumer<Result<List<StreamDataOperation>>> {
+  class MutationCommitter extends HeadMutationCommitter implements Consumer<Result<Model>> {
 
     private final String task;
     private final MutationContext mutationContext;
@@ -239,7 +239,7 @@
     }
 
     @Override
-    public void accept(Result<List<StreamDataOperation>> updateResults) {
+    public void accept(Result<Model> updateResults) {
       if (!updateResults.isSuccessful()) {
         errorCount++;
         Session session = null;
@@ -264,7 +264,7 @@
         }
         return;
       }
-      dataOperations = updateResults.getValue();
+      dataOperations = updateResults.getValue().streamDataOperations;
       for (StreamDataOperation operation : dataOperations) {
         if (operation.getStreamStructure().getOperation() == Operation.CLEAR_ALL) {
           clearedHead = true;
diff --git a/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/BUILD b/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/BUILD
index 3e39637..0a627e8 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/BUILD
@@ -7,6 +7,7 @@
     testonly = True,
     srcs = glob(["*.java"]),
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
diff --git a/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/FakeProtocolAdapter.java b/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/FakeProtocolAdapter.java
index 05f1f4e..7f0af43 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/FakeProtocolAdapter.java
+++ b/src/main/java/com/google/android/libraries/feed/testing/protocoladapter/FakeProtocolAdapter.java
@@ -14,6 +14,7 @@
 
 package com.google.android.libraries.feed.testing.protocoladapter;
 
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.common.Result;
 import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
@@ -33,9 +34,9 @@
   /*@Nullable*/ private Response lastResponse = null;
 
   @Override
-  public Result<List<StreamDataOperation>> createModel(Response response) {
+  public Result<Model> createModel(Response response) {
     lastResponse = response;
-    return Result.success(new ArrayList<>());
+    return Result.success(Model.empty());
   }
 
   @Override
diff --git a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD
index 2f44363..2492b5b 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD
@@ -8,6 +8,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//src/main/java/com/google/android/libraries/feed/api/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/api/internal/requestmanager",
         "//src/main/java/com/google/android/libraries/feed/common",
diff --git a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeFeedRequestManager.java b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeFeedRequestManager.java
index af3f599..29562e1 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeFeedRequestManager.java
+++ b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeFeedRequestManager.java
@@ -16,6 +16,7 @@
 
 import com.google.android.libraries.feed.api.host.logging.RequestReason;
 import com.google.android.libraries.feed.api.host.logging.Task;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.api.internal.requestmanager.FeedRequestManager;
 import com.google.android.libraries.feed.common.Result;
@@ -24,12 +25,10 @@
 import com.google.android.libraries.feed.common.concurrent.TaskQueue.TaskType;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.functional.Consumer;
-import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
 import com.google.search.now.feed.client.StreamDataProto.StreamToken;
 import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
 import com.google.search.now.wire.feed.ResponseProto.Response;
 import java.util.ArrayDeque;
-import java.util.List;
 import java.util.Queue;
 
 /**
@@ -82,25 +81,20 @@
 
   @Override
   public void loadMore(
-      StreamToken streamToken,
-      ConsistencyToken token,
-      Consumer<Result<List<StreamDataOperation>>> consumer) {
+      StreamToken streamToken, ConsistencyToken token, Consumer<Result<Model>> consumer) {
     fakeThreadUtils.checkNotMainThread();
     latestStreamToken = streamToken;
     handleResponseWithDelay(responses.remove(), consumer);
   }
 
   @Override
-  public void triggerRefresh(
-      @RequestReason int reason, Consumer<Result<List<StreamDataOperation>>> consumer) {
+  public void triggerRefresh(@RequestReason int reason, Consumer<Result<Model>> consumer) {
     triggerRefresh(reason, ConsistencyToken.getDefaultInstance(), consumer);
   }
 
   @Override
   public void triggerRefresh(
-      @RequestReason int reason,
-      ConsistencyToken token,
-      Consumer<Result<List<StreamDataOperation>>> consumer) {
+      @RequestReason int reason, ConsistencyToken token, Consumer<Result<Model>> consumer) {
     latestRequestReason = reason;
     ResponseWithDelay responseWithDelay = responses.remove();
     taskQueue.execute(
@@ -112,7 +106,7 @@
   }
 
   private void handleResponseWithDelay(
-      ResponseWithDelay responseWithDelay, Consumer<Result<List<StreamDataOperation>>> consumer) {
+      ResponseWithDelay responseWithDelay, Consumer<Result<Model>> consumer) {
     if (responseWithDelay.delayMs > 0) {
       mainThreadRunner.executeWithDelay(
           "FakeFeedRequestManager#consumer",
@@ -126,7 +120,7 @@
   }
 
   private void invokeConsumer(
-      ResponseWithDelay responseWithDelay, Consumer<Result<List<StreamDataOperation>>> consumer) {
+      ResponseWithDelay responseWithDelay, Consumer<Result<Model>> consumer) {
     boolean policy = fakeThreadUtils.enforceMainThread(true);
     if (responseWithDelay.isError) {
       consumer.accept(Result.failure());
diff --git a/src/test/java/com/google/android/libraries/feed/feedactionmanager/BUILD b/src/test/java/com/google/android/libraries/feed/feedactionmanager/BUILD
index c446c4a..dcb9442 100644
--- a/src/test/java/com/google/android/libraries/feed/feedactionmanager/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedactionmanager/BUILD
@@ -12,6 +12,7 @@
     deps = [
         "//src/main/java/com/google/android/libraries/feed/api/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/actionmanager",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/sessionmanager",
         "//src/main/java/com/google/android/libraries/feed/api/internal/store",
         "//src/main/java/com/google/android/libraries/feed/common",
diff --git a/src/test/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImplTest.java b/src/test/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImplTest.java
index d292131..469be53 100644
--- a/src/test/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImplTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedactionmanager/FeedActionManagerImplTest.java
@@ -23,6 +23,7 @@
 
 import com.google.android.libraries.feed.api.common.MutationContext;
 import com.google.android.libraries.feed.api.internal.actionmanager.ActionManager;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.sessionmanager.FeedSessionManager;
 import com.google.android.libraries.feed.api.internal.store.LocalActionMutation;
 import com.google.android.libraries.feed.api.internal.store.LocalActionMutation.ActionType;
@@ -69,10 +70,10 @@
   @Mock private FeedSessionManager feedSessionManager;
   @Mock private Store store;
   @Mock private LocalActionMutation localActionMutation;
-  @Mock private Consumer<Result<List<StreamDataOperation>>> streamDataOperationsConsumer;
+  @Mock private Consumer<Result<Model>> modelConsumer;
   @Captor private ArgumentCaptor<Integer> actionTypeCaptor;
   @Captor private ArgumentCaptor<String> contentIdStringCaptor;
-  @Captor private ArgumentCaptor<Result<List<StreamDataOperation>>> streamDataOperationsCaptor;
+  @Captor private ArgumentCaptor<Result<Model>> modelCaptor;
   @Captor private ArgumentCaptor<MutationContext> mutationContextCaptor;
   @Captor private ArgumentCaptor<Consumer<Result<ConsistencyToken>>> consumerCaptor;
   @Captor private ArgumentCaptor<Set<StreamUploadableAction>> actionCaptor;
@@ -108,10 +109,10 @@
 
     verify(localActionMutation).commit();
 
-    verify(streamDataOperationsConsumer).accept(streamDataOperationsCaptor.capture());
-    Result<List<StreamDataOperation>> result = streamDataOperationsCaptor.getValue();
+    verify(modelConsumer).accept(modelCaptor.capture());
+    Result<Model> result = modelCaptor.getValue();
     assertThat(result.isSuccessful()).isTrue();
-    List<StreamDataOperation> streamDataOperations = result.getValue();
+    List<StreamDataOperation> streamDataOperations = result.getValue().streamDataOperations;
     assertThat(streamDataOperations).hasSize(1);
     StreamDataOperation streamDataOperation = streamDataOperations.get(0);
     assertThat(streamDataOperation).isEqualTo(dataOperation);
@@ -136,10 +137,10 @@
     verify(feedSessionManager).getUpdateConsumer(mutationContextCaptor.capture());
     assertThat(mutationContextCaptor.getValue().getRequestingSessionId()).isEqualTo(SESSION_ID);
 
-    verify(streamDataOperationsConsumer).accept(streamDataOperationsCaptor.capture());
-    Result<List<StreamDataOperation>> result = streamDataOperationsCaptor.getValue();
+    verify(modelConsumer).accept(modelCaptor.capture());
+    Result<Model> result = modelCaptor.getValue();
     assertThat(result.isSuccessful()).isTrue();
-    List<StreamDataOperation> streamDataOperations = result.getValue();
+    List<StreamDataOperation> streamDataOperations = result.getValue().streamDataOperations;
     assertThat(streamDataOperations).hasSize(1);
     StreamDataOperation streamDataOperation = streamDataOperations.get(0);
     assertThat(streamDataOperation).isEqualTo(dataOperation);
@@ -152,10 +153,10 @@
 
     actionManager.dismiss(Collections.singletonList(dataOperation), null);
 
-    verify(streamDataOperationsConsumer).accept(streamDataOperationsCaptor.capture());
-    Result<List<StreamDataOperation>> result = streamDataOperationsCaptor.getValue();
+    verify(modelConsumer).accept(modelCaptor.capture());
+    Result<Model> result = modelCaptor.getValue();
     assertThat(result.isSuccessful()).isTrue();
-    List<StreamDataOperation> streamDataOperations = result.getValue();
+    List<StreamDataOperation> streamDataOperations = result.getValue().streamDataOperations;
     assertThat(streamDataOperations).hasSize(1);
     StreamDataOperation streamDataOperation = streamDataOperations.get(0);
     assertThat(streamDataOperation).isEqualTo(dataOperation);
@@ -171,10 +172,10 @@
     verify(feedSessionManager).getUpdateConsumer(mutationContextCaptor.capture());
     assertThat(mutationContextCaptor.getValue().getRequestingSessionId()).isEqualTo(SESSION_ID);
 
-    verify(streamDataOperationsConsumer).accept(streamDataOperationsCaptor.capture());
-    Result<List<StreamDataOperation>> result = streamDataOperationsCaptor.getValue();
+    verify(modelConsumer).accept(modelCaptor.capture());
+    Result<Model> result = modelCaptor.getValue();
     assertThat(result.isSuccessful()).isTrue();
-    List<StreamDataOperation> streamDataOperations = result.getValue();
+    List<StreamDataOperation> streamDataOperations = result.getValue().streamDataOperations;
     assertThat(streamDataOperations).hasSize(1);
     StreamDataOperation streamDataOperation = streamDataOperations.get(0);
     assertThat(streamDataOperation).isEqualTo(dataOperation);
@@ -219,7 +220,7 @@
 
   private void setUpDismissMocks() {
     when(feedSessionManager.getUpdateConsumer(any(MutationContext.class)))
-        .thenReturn(streamDataOperationsConsumer);
+        .thenReturn(modelConsumer);
     when(localActionMutation.add(anyInt(), anyString())).thenReturn(localActionMutation);
     when(store.editLocalActions()).thenReturn(localActionMutation);
   }
diff --git a/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD b/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD
index 78756b3..26501cc 100644
--- a/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/BUILD
@@ -10,6 +10,7 @@
     aapt_version = "aapt2",
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/testing",
diff --git a/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapterTest.java b/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapterTest.java
index ce6ad96..187a19e 100644
--- a/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapterTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedprotocoladapter/FeedProtocolAdapterTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.protocoladapter.RequiredContentAdapter;
 import com.google.android.libraries.feed.common.Result;
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
@@ -110,20 +111,20 @@
   @Test
   public void testSimpleResponse_clear() {
     Response response = responseBuilder.addClearOperation().build();
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
-    assertThat(results.getValue()).hasSize(1);
+    assertThat(results.getValue().streamDataOperations).hasSize(1);
   }
 
   @Test
   public void testSimpleResponse_feature() {
     Response response = responseBuilder.addRootFeature().build();
 
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
-    assertThat(results.getValue()).hasSize(1);
+    assertThat(results.getValue().streamDataOperations).hasSize(1);
 
-    StreamDataOperation sdo = results.getValue().get(0);
+    StreamDataOperation sdo = results.getValue().streamDataOperations.get(0);
     assertThat(sdo.hasStreamPayload()).isTrue();
     assertThat(sdo.getStreamPayload().hasStreamFeature()).isTrue();
     assertThat(sdo.hasStreamStructure()).isTrue();
@@ -139,11 +140,11 @@
     Response response =
         new ResponseBuilder().addCardWithSemanticData(contentId, semanticData).build();
 
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
     // Note that 2 operations are created (the card and the semantic data). We want the latter.
-    assertThat(results.getValue()).hasSize(2);
-    StreamDataOperation sdo = results.getValue().get(1);
+    assertThat(results.getValue().streamDataOperations).hasSize(2);
+    StreamDataOperation sdo = results.getValue().streamDataOperations.get(1);
     assertThat(sdo.getStreamPayload().hasSemanticData()).isTrue();
     assertThat(sdo.getStreamPayload().getSemanticData()).isEqualTo(semanticData);
   }
@@ -154,11 +155,11 @@
     OpaqueActionData actionData = OpaqueActionData.getDefaultInstance();
     Response response = new ResponseBuilder().addCardWithActionData(contentId, actionData).build();
 
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
     // Note that 2 operations are created (the card and the action data). We want the latter.
-    assertThat(results.getValue()).hasSize(2);
-    StreamDataOperation sdo = results.getValue().get(1);
+    assertThat(results.getValue().streamDataOperations).hasSize(2);
+    StreamDataOperation sdo = results.getValue().streamDataOperations.get(1);
     assertThat(sdo.getStreamPayload().hasActionData()).isTrue();
     assertThat(sdo.getStreamPayload().getActionData()).isEqualTo(actionData);
   }
@@ -175,9 +176,9 @@
             .addCard(cardId, clusterId)
             .build();
 
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
-    List<StreamDataOperation> operations = results.getValue();
+    List<StreamDataOperation> operations = results.getValue().streamDataOperations;
 
     assertThat(operations).hasSize(4);
     assertThat(operations.get(0).getStreamPayload().getStreamFeature().hasStream()).isTrue();
@@ -192,18 +193,18 @@
         responseBuilder
             .removeFeature(ContentId.getDefaultInstance(), ContentId.getDefaultInstance())
             .build();
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
-    assertThat(results.getValue()).hasSize(1);
+    assertThat(results.getValue().streamDataOperations).hasSize(1);
   }
 
   @Test
   public void testPietSharedState() {
     Response response = responseBuilder.addPietSharedState().build();
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
-    assertThat(results.getValue()).hasSize(1);
-    StreamDataOperation sdo = results.getValue().get(0);
+    assertThat(results.getValue().streamDataOperations).hasSize(1);
+    StreamDataOperation sdo = results.getValue().streamDataOperations.get(0);
     assertThat(sdo.hasStreamPayload()).isTrue();
     assertThat(sdo.getStreamPayload().hasStreamSharedState()).isTrue();
     assertThat(sdo.hasStreamStructure()).isTrue();
@@ -215,10 +216,10 @@
     ByteString tokenForMutation = ByteString.copyFrom("token", Charset.defaultCharset());
     Response response = responseBuilder.addStreamToken(1, tokenForMutation).build();
 
-    Result<List<StreamDataOperation>> results = protocolAdapter.createModel(response);
+    Result<Model> results = protocolAdapter.createModel(response);
     assertThat(results.isSuccessful()).isTrue();
-    assertThat(results.getValue()).hasSize(1);
-    StreamDataOperation sdo = results.getValue().get(0);
+    assertThat(results.getValue().streamDataOperations).hasSize(1);
+    StreamDataOperation sdo = results.getValue().streamDataOperations.get(0);
     assertThat(sdo.hasStreamPayload()).isTrue();
     assertThat(sdo.getStreamPayload().hasStreamToken()).isTrue();
   }
@@ -242,11 +243,11 @@
 
     FeedProtocolAdapter protocolAdapter =
         new FeedProtocolAdapter(ImmutableList.of(adapter), timingUtils);
-    Result<List<StreamDataOperation>> result = protocolAdapter.createModel(response);
+    Result<Model> result = protocolAdapter.createModel(response);
 
     verify(adapter, times(4)).determineRequiredContentIds(any(DataOperation.class));
     assertThat(result.isSuccessful()).isTrue();
-    List<StreamDataOperation> operations = result.getValue();
+    List<StreamDataOperation> operations = result.getValue().streamDataOperations;
     assertThat(operations).hasSize(7);
     assertThat(operations.get(4).getStreamStructure().getOperation())
         .isEqualTo(StreamStructure.Operation.REQUIRED_CONTENT);
diff --git a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD
index 9fbe087..96ab241 100644
--- a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD
@@ -43,12 +43,12 @@
     deps = [
         "//src/main/java/com/google/android/libraries/feed/api/common",
         "//src/main/java/com/google/android/libraries/feed/api/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/requestmanager",
         "//src/main/java/com/google/android/libraries/feed/api/internal/sessionmanager",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/java/com/google/android/libraries/feed/feedrequestmanager",
-        "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
         "//third_party:robolectric",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
         "@maven//:org_mockito_mockito_core",
diff --git a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImplTest.java b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImplTest.java
index 9396395..b7cd342 100644
--- a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImplTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedRequestManagerImplTest.java
@@ -36,6 +36,7 @@
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.host.stream.TooltipInfo.FeatureName;
 import com.google.android.libraries.feed.api.internal.common.DismissActionWithSemanticProperties;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.common.Result;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeTaskQueue;
@@ -53,7 +54,6 @@
 import com.google.protobuf.ByteString;
 import com.google.protobuf.CodedOutputStream;
 import com.google.protobuf.ExtensionRegistryLite;
-import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
 import com.google.search.now.feed.client.StreamDataProto.StreamToken;
 import com.google.search.now.wire.feed.ActionTypeProto.ActionType;
 import com.google.search.now.wire.feed.CapabilityProto.Capability;
@@ -125,8 +125,8 @@
   private FakeBasicLoggingApi fakeBasicLoggingApi;
   private FakeNetworkClient fakeNetworkClient;
   private FakeTooltipSupportedApi fakeTooltipSupportedApi;
-  private RequiredConsumer<Result<List<StreamDataOperation>>> consumer;
-  private Result<List<StreamDataOperation>> consumedResult = Result.failure();
+  private RequiredConsumer<Result<Model>> consumer;
+  private Result<Model> consumedResult = Result.failure();
   private HttpResponse failingResponse;
 
   @Before
diff --git a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/RequestManagerImplTest.java b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/RequestManagerImplTest.java
index 62dc018..d49db45 100644
--- a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/RequestManagerImplTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/RequestManagerImplTest.java
@@ -20,12 +20,11 @@
 
 import com.google.android.libraries.feed.api.common.MutationContext;
 import com.google.android.libraries.feed.api.host.logging.RequestReason;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.requestmanager.FeedRequestManager;
 import com.google.android.libraries.feed.api.internal.sessionmanager.FeedSessionManager;
 import com.google.android.libraries.feed.common.Result;
 import com.google.android.libraries.feed.common.functional.Consumer;
-import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
-import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,7 +37,7 @@
 
   @Mock private FeedRequestManager feedRequestManager;
   @Mock private FeedSessionManager feedSessionManager;
-  @Mock private Consumer<Result<List<StreamDataOperation>>> updateConsumer;
+  @Mock private Consumer<Result<Model>> updateConsumer;
 
   private RequestManagerImpl requestManager;
 
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java
index 172f188..0f04a4b 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerImplTest.java
@@ -30,6 +30,7 @@
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi.RequestBehavior;
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi.SessionState;
 import com.google.android.libraries.feed.api.internal.common.ActionPropertiesWithId;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.SemanticPropertiesWithId;
 import com.google.android.libraries.feed.api.internal.common.testing.ContentIdGenerators;
 import com.google.android.libraries.feed.api.internal.common.testing.InternalProtocolBuilder;
@@ -458,12 +459,11 @@
   public void testUpdateConsumer() {
     FeedSessionManagerImpl sessionManager = getInitializedSessionManager();
     assertThat(sessionManager.outstandingMutations).isEmpty();
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        sessionManager.getUpdateConsumer(EMPTY_MUTATION);
+    Consumer<Result<Model>> updateConsumer = sessionManager.getUpdateConsumer(EMPTY_MUTATION);
     assertThat(updateConsumer).isInstanceOf(SessionMutationTracker.class);
     assertThat(sessionManager.outstandingMutations).hasSize(1);
     assertThat(sessionManager.outstandingMutations).contains(updateConsumer);
-    updateConsumer.accept(Result.success(new ArrayList<>()));
+    updateConsumer.accept(Result.success(Model.empty()));
     assertThat(sessionManager.outstandingMutations).isEmpty();
   }
 
@@ -471,14 +471,13 @@
   public void testUpdateConsumer_clearAll() {
     FeedSessionManagerImpl sessionManager = getInitializedSessionManager();
     assertThat(sessionManager.outstandingMutations).isEmpty();
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        sessionManager.getUpdateConsumer(EMPTY_MUTATION);
+    Consumer<Result<Model>> updateConsumer = sessionManager.getUpdateConsumer(EMPTY_MUTATION);
     assertThat(sessionManager.outstandingMutations).hasSize(1);
     appLifecycleListener.onClearAll();
     assertThat(sessionManager.outstandingMutations).isEmpty();
 
     // verify this still runs (as a noop)
-    updateConsumer.accept(Result.success(new ArrayList<>()));
+    updateConsumer.accept(Result.success(Model.empty()));
     assertThat(sessionManager.outstandingMutations).isEmpty();
   }
 
@@ -486,14 +485,13 @@
   public void testUpdateConsumer_clearAllWithRefresh() {
     FeedSessionManagerImpl sessionManager = getInitializedSessionManager();
     assertThat(sessionManager.outstandingMutations).isEmpty();
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        sessionManager.getUpdateConsumer(EMPTY_MUTATION);
+    Consumer<Result<Model>> updateConsumer = sessionManager.getUpdateConsumer(EMPTY_MUTATION);
     assertThat(sessionManager.outstandingMutations).hasSize(1);
     appLifecycleListener.onClearAllWithRefresh();
     assertThat(sessionManager.outstandingMutations).isEmpty();
 
     // verify this still runs (as a noop)
-    updateConsumer.accept(Result.success(new ArrayList<>()));
+    updateConsumer.accept(Result.success(Model.empty()));
     assertThat(sessionManager.outstandingMutations).isEmpty();
   }
 
@@ -516,9 +514,8 @@
                     .setOperation(Operation.UPDATE_OR_APPEND))
             .build();
 
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        sessionManager.getUpdateConsumer(EMPTY_MUTATION);
-    Result<List<StreamDataOperation>> result = Result.success(listOf(streamDataOperation));
+    Consumer<Result<Model>> updateConsumer = sessionManager.getUpdateConsumer(EMPTY_MUTATION);
+    Result<Model> result = Result.success(Model.of(listOf(streamDataOperation)));
     updateConsumer.accept(result);
 
     assertThat(fakeStore.getContentById(rootContentId))
@@ -539,9 +536,8 @@
                     .setOperation(Operation.UPDATE_OR_APPEND))
             .build();
 
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        sessionManager.getUpdateConsumer(EMPTY_MUTATION);
-    Result<List<StreamDataOperation>> result = Result.success(listOf(streamDataOperation));
+    Consumer<Result<Model>> updateConsumer = sessionManager.getUpdateConsumer(EMPTY_MUTATION);
+    Result<Model> result = Result.success(Model.of(listOf(streamDataOperation)));
     updateConsumer.accept(result);
 
     assertThat(fakeStore.getContentById(rootContentId))
@@ -775,9 +771,8 @@
       internalProtocolBuilder.addSharedState(sharedStateId);
       operationCount++;
     }
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        sessionManager.getUpdateConsumer(EMPTY_MUTATION);
-    updateConsumer.accept(Result.success(internalProtocolBuilder.build()));
+    Consumer<Result<Model>> updateConsumer = sessionManager.getUpdateConsumer(EMPTY_MUTATION);
+    updateConsumer.accept(Result.success(Model.of(internalProtocolBuilder.build())));
     return operationCount;
   }
 
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java
index 13a3b9b..f3cc34b 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionManagerMutationTest.java
@@ -31,6 +31,7 @@
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.host.storage.ContentStorageDirect;
 import com.google.android.libraries.feed.api.internal.common.ActionPropertiesWithId;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
 import com.google.android.libraries.feed.api.internal.common.SemanticPropertiesWithId;
 import com.google.android.libraries.feed.api.internal.common.testing.ContentIdGenerators;
@@ -161,7 +162,7 @@
     MutationCommitter mutationCommitter =
         getMutationCommitter(
             new MutationContext.Builder().setRequestingSessionId(sessionId).build());
-    mutationCommitter.accept(Result.success(dataOperations));
+    mutationCommitter.accept(Result.success(Model.of(dataOperations)));
     assertThat(mutationCommitter.clearedHead).isTrue();
     verify(schedulerApi).onReceiveNewContent(5L);
     verify(knownContentListener).onNewContentReceived(true, 5L);
@@ -183,7 +184,7 @@
       dataOperations.add(getStreamDataOperation(feature.first, feature.second));
     }
     MutationCommitter mutationCommitter = getMutationCommitter(EMPTY_CONTEXT);
-    mutationCommitter.accept(Result.success(dataOperations));
+    mutationCommitter.accept(Result.success(Model.of(dataOperations)));
 
     Result<List<PayloadWithId>> result = storeSpy.getPayloads(contentIds);
     assertThat(result.isSuccessful()).isTrue();
@@ -211,7 +212,7 @@
       dataOperations.add(getStreamDataOperation(feature.first, feature.second));
     }
     MutationCommitter mutationCommitter = getMutationCommitter(EMPTY_CONTEXT);
-    mutationCommitter.accept(Result.success(dataOperations));
+    mutationCommitter.accept(Result.success(Model.of(dataOperations)));
 
     Result<List<SemanticPropertiesWithId>> result = storeSpy.getSemanticProperties(contentIds);
     assertThat(result.isSuccessful()).isTrue();
@@ -237,7 +238,7 @@
       dataOperations.add(getStreamDataOperation(feature.first, feature.second));
     }
     MutationCommitter mutationCommitter = getMutationCommitter(EMPTY_CONTEXT);
-    mutationCommitter.accept(Result.success(dataOperations));
+    mutationCommitter.accept(Result.success(Model.of(dataOperations)));
 
     Result<List<ActionPropertiesWithId>> result = storeSpy.getActionProperties(contentIds);
     assertThat(result.isSuccessful()).isTrue();
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD b/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
index 74ad893..4992cba 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
@@ -377,6 +377,7 @@
     deps = [
         "//src/main/java/com/google/android/libraries/feed/api/client/requestmanager",
         "//src/main/java/com/google/android/libraries/feed/api/common",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/modelprovider",
         "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/api/internal/sessionmanager",
@@ -549,6 +550,7 @@
         "//src/main/java/com/google/android/libraries/feed/api/common",
         "//src/main/java/com/google/android/libraries/feed/api/host/config",
         "//src/main/java/com/google/android/libraries/feed/api/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common/testing",
         "//src/main/java/com/google/android/libraries/feed/api/internal/modelprovider",
         "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/RemoveTrackingBehaviorTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/RemoveTrackingBehaviorTest.java
index 898b622..62ab27b 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/RemoveTrackingBehaviorTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/RemoveTrackingBehaviorTest.java
@@ -20,6 +20,7 @@
 
 import com.google.android.libraries.feed.api.client.requestmanager.RequestManager;
 import com.google.android.libraries.feed.api.common.MutationContext;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider.RemoveTrackingFactory;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelProviderFactory;
@@ -90,9 +91,8 @@
         ResponseBuilder.builder().removeFeature(CARDS[1], ROOT_CONTENT_ID);
     List<StreamDataOperation> dataOperations = getDataOperations(responseBuilder);
     MutationContext mutationContext = new MutationContext.Builder().setUserInitiated(true).build();
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        feedSessionManager.getUpdateConsumer(mutationContext);
-    updateConsumer.accept(Result.success(dataOperations));
+    Consumer<Result<Model>> updateConsumer = feedSessionManager.getUpdateConsumer(mutationContext);
+    updateConsumer.accept(Result.success(Model.of(dataOperations)));
     assertThat(called.get()).isTrue();
   }
 
@@ -115,9 +115,8 @@
             .removeFeature(CARDS[3], ROOT_CONTENT_ID);
     List<StreamDataOperation> dataOperations = getDataOperations(responseBuilder);
     MutationContext mutationContext = new MutationContext.Builder().setUserInitiated(true).build();
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        feedSessionManager.getUpdateConsumer(mutationContext);
-    updateConsumer.accept(Result.success(dataOperations));
+    Consumer<Result<Model>> updateConsumer = feedSessionManager.getUpdateConsumer(mutationContext);
+    updateConsumer.accept(Result.success(Model.of(dataOperations)));
     assertThat(called.get()).isTrue();
   }
 
@@ -143,9 +142,8 @@
         ResponseBuilder.builder().removeFeature(CARDS[1], ROOT_CONTENT_ID);
     List<StreamDataOperation> dataOperations = getDataOperations(responseBuilder);
     MutationContext mutationContext = new MutationContext.Builder().setUserInitiated(false).build();
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        feedSessionManager.getUpdateConsumer(mutationContext);
-    updateConsumer.accept(Result.success(dataOperations));
+    Consumer<Result<Model>> updateConsumer = feedSessionManager.getUpdateConsumer(mutationContext);
+    updateConsumer.accept(Result.success(Model.of(dataOperations)));
     assertThat(called.get()).isFalse();
   }
 
@@ -181,9 +179,9 @@
 
   private List<StreamDataOperation> getDataOperations(ResponseBuilder builder) {
     Response response = builder.build();
-    Result<List<StreamDataOperation>> result = protocolAdapter.createModel(response);
+    Result<Model> result = protocolAdapter.createModel(response);
     assertThat(result.isSuccessful()).isTrue();
-    return result.getValue();
+    return result.getValue().streamDataOperations;
   }
 
   private void loadInitialData() {
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java
index f60ba53..658e251 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java
@@ -24,6 +24,7 @@
 import com.google.android.libraries.feed.api.host.config.Configuration;
 import com.google.android.libraries.feed.api.host.config.Configuration.ConfigKey;
 import com.google.android.libraries.feed.api.host.logging.RequestReason;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.common.testing.ContentIdGenerators;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelChild;
 import com.google.android.libraries.feed.api.internal.modelprovider.ModelCursor;
@@ -45,11 +46,9 @@
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
 import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.testing.requestmanager.FakeFeedRequestManager;
-import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import java.util.Arrays;
-import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -474,13 +473,12 @@
         new MutationContext.Builder().setRequestingSessionId(modelProvider.getSessionId()).build();
 
     // Remove the single item under the root to create the empty state.
-    Result<List<StreamDataOperation>> result =
+    Result<Model> result =
         protocolAdapter.createModel(
             ResponseBuilder.builder().removeFeature(cards[0], ROOT_CONTENT_ID).build());
     assertThat(result.isSuccessful()).isTrue();
-    Consumer<Result<List<StreamDataOperation>>> updateConsumer =
-        feedSessionManager.getUpdateConsumer(context);
-    updateConsumer.accept(Result.success(result.getValue()));
+    Consumer<Result<Model>> updateConsumer = feedSessionManager.getUpdateConsumer(context);
+    updateConsumer.accept(result);
 
     // Verify the empty state
     ModelFeature root = modelProvider.getRootFeature();
@@ -493,7 +491,7 @@
         protocolAdapter.createModel(ResponseBuilder.builder().addCardsToRoot(cardsTwo).build());
     assertThat(result.isSuccessful()).isTrue();
     updateConsumer = feedSessionManager.getUpdateConsumer(context);
-    updateConsumer.accept(Result.success(result.getValue()));
+    updateConsumer.accept(result);
 
     // Validate the current model
     root = modelProvider.getRootFeature();
diff --git a/src/test/java/com/google/android/libraries/feed/mocknetworkclient/BUILD b/src/test/java/com/google/android/libraries/feed/mocknetworkclient/BUILD
index 732e97b..ea00977 100644
--- a/src/test/java/com/google/android/libraries/feed/mocknetworkclient/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/mocknetworkclient/BUILD
@@ -15,6 +15,7 @@
         "//src/main/java/com/google/android/libraries/feed/api/host/scheduler",
         "//src/main/java/com/google/android/libraries/feed/api/host/stream",
         "//src/main/java/com/google/android/libraries/feed/api/internal/actionmanager",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/concurrent",
diff --git a/src/test/java/com/google/android/libraries/feed/mocknetworkclient/MockServerNetworkClientTest.java b/src/test/java/com/google/android/libraries/feed/mocknetworkclient/MockServerNetworkClientTest.java
index ae6eec9..ca11c04 100644
--- a/src/test/java/com/google/android/libraries/feed/mocknetworkclient/MockServerNetworkClientTest.java
+++ b/src/test/java/com/google/android/libraries/feed/mocknetworkclient/MockServerNetworkClientTest.java
@@ -31,6 +31,7 @@
 import com.google.android.libraries.feed.api.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.api.host.stream.TooltipSupportedApi;
 import com.google.android.libraries.feed.api.internal.actionmanager.ActionReader;
+import com.google.android.libraries.feed.api.internal.common.Model;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.common.Result;
 import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
@@ -186,7 +187,7 @@
             basicLoggingApi,
             tooltipSupportedApi);
     when(protocolAdapter.createModel(any(Response.class)))
-        .thenReturn(Result.success(new ArrayList<>()));
+        .thenReturn(Result.success(Model.empty()));
 
     fakeThreadUtils.enforceMainThread(false);
     feedRequestManager.loadMore(
@@ -194,7 +195,7 @@
         ConsistencyToken.getDefaultInstance(),
         result -> {
           assertThat(result.isSuccessful()).isTrue();
-          assertThat(result.getValue()).hasSize(0);
+          assertThat(result.getValue().streamDataOperations).hasSize(0);
         });
 
     verify(protocolAdapter).createModel(responseCaptor.capture());
@@ -230,7 +231,7 @@
             basicLoggingApi,
             tooltipSupportedApi);
     when(protocolAdapter.createModel(any(Response.class)))
-        .thenReturn(Result.success(new ArrayList<>()));
+        .thenReturn(Result.success(Model.empty()));
 
     fakeThreadUtils.enforceMainThread(false);
     ByteString token = ByteString.copyFromUtf8("fooToken");
@@ -240,7 +241,7 @@
         ConsistencyToken.getDefaultInstance(),
         result -> {
           assertThat(result.isSuccessful()).isTrue();
-          assertThat(result.getValue()).hasSize(0);
+          assertThat(result.getValue().streamDataOperations).hasSize(0);
         });
 
     verify(protocolAdapter).createModel(responseCaptor.capture());