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

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new d4ceac119 [runtime][java] Preserve an existing KeyedStream's business 
key when no key selector is given (#1312)
d4ceac119 is described below

commit d4ceac119453c6b6d782ca164a76f67263fd016f
Author: wank125 <[email protected]>
AuthorDate: Sat Oct 24 17:36:30 2026 +0800

    [runtime][java] Preserve an existing KeyedStream's business key when no key 
selector is given (#1312)
    
    Co-authored-by: wank125 <[email protected]>
    Co-authored-by: WenjinXie <[email protected]>
---
 .../agents/api/AgentsExecutionEnvironment.java     |  23 ++-
 .../docs/development/integrate_with_flink.md       |  13 +-
 .../quickstart/create-agent/multiple-agents.md     |  21 ++-
 .../quickstart/create-agent/single-agent.md        |  31 ++--
 .../docs/get-started/quickstart/react_agent.md     |   2 +-
 .../docs/get-started/quickstart/yaml_agent.md      |  22 ++-
 .../integration/test/FlinkIntegrationTest.java     |   6 +-
 .../flink/agents/examples/ModelRoutingExample.java |   2 +-
 .../agents/examples/ModelRoutingJudgeExample.java  |   2 +-
 .../flink/agents/examples/ReActAgentExample.java   |   2 +-
 .../flink/agents/examples/SkillsAgentExample.java  |   5 +-
 .../examples/WorkflowMultipleAgentExample.java     |  16 +-
 .../examples/WorkflowSingleAgentExample.java       |  24 ++-
 .../agents/examples/YamlWorkflowAgentExample.java  |  24 ++-
 .../examples/agents/ProductSuggestionAgent.java    |  12 +-
 .../examples/agents/ReviewAnalysisAgent.java       |  10 +-
 .../examples/agents/TableReviewAnalysisAgent.java  |   2 +-
 .../examples/openai/OpenAiModelRoutingExample.java |   2 +-
 python/flink_agents/api/execution_environment.py   |   5 +-
 .../runtime/remote_execution_environment.py        |  31 ++--
 .../tests/test_remote_execution_environment.py     |  50 +++++-
 .../runtime/env/RemoteExecutionEnvironment.java    |  23 ++-
 .../env/RemoteExecutionEnvironmentTest.java        | 171 +++++++++++++++++++++
 .../runtime/env/TypedOutputTerminalTest.java       |   4 +-
 24 files changed, 394 insertions(+), 109 deletions(-)

diff --git 
a/api/src/main/java/org/apache/flink/agents/api/AgentsExecutionEnvironment.java 
b/api/src/main/java/org/apache/flink/agents/api/AgentsExecutionEnvironment.java
index 9095f23dd..8568f94d8 100644
--- 
a/api/src/main/java/org/apache/flink/agents/api/AgentsExecutionEnvironment.java
+++ 
b/api/src/main/java/org/apache/flink/agents/api/AgentsExecutionEnvironment.java
@@ -133,11 +133,13 @@ public abstract class AgentsExecutionEnvironment {
      * Set input for agents from a DataStream. Used for remote execution.
      *
      * <p>This method integrates agents with Flink DataStream API, allowing 
agents to process
-     * streaming data with optional keying for stateful operations.
+     * streaming data keyed for stateful operations.
      *
      * @param input DataStream to be processed by agents.
-     * @param keySelector Optional KeySelector for extracting keys from input 
records. If null, the
-     *     stream will be processed without keying.
+     * @param keySelector Optional KeySelector for extracting keys from input 
records. An explicit
+     *     selector is always used, even when the input is already keyed; if 
null, the input must be
+     *     a KeyedStream, whose key is kept, otherwise building the pipeline 
fails with a clear
+     *     error rather than silently keying by the whole record.
      * @param <T> Type of elements in the input DataStream.
      * @param <K> Type of the key extracted by the KeySelector.
      * @return AgentBuilder for configuring the agent pipeline.
@@ -146,7 +148,10 @@ public abstract class AgentsExecutionEnvironment {
             DataStream<T> input, KeySelector<T, K> keySelector);
 
     /**
-     * Set input for agents from a DataStream without keying.
+     * Set input for agents from a DataStream, keeping the stream's own key.
+     *
+     * <p>The input must be a KeyedStream; its existing key is preserved. A 
non-keyed input is
+     * rejected with a clear error asking for a keyed stream or a key selector.
      *
      * @param input DataStream to be processed by agents.
      * @param <T> Type of elements in the input DataStream.
@@ -163,17 +168,23 @@ public abstract class AgentsExecutionEnvironment {
      * and processing it through agents.
      *
      * @param input Table to be processed by agents.
-     * @param keySelector Optional KeySelector for extracting keys from table 
rows.
+     * @param keySelector Required KeySelector for extracting the business key 
from table rows. Rows
+     *     with the same key share the agent's short-term memory.
      * @param <K> Type of the key extracted by the KeySelector.
      * @return AgentBuilder for configuring the agent pipeline.
+     * @throws IllegalArgumentException in remote execution if keySelector is 
null.
      */
     public abstract <K> AgentBuilder fromTable(Table input, 
KeySelector<Object, K> keySelector);
 
     /**
-     * Set input for agents from a Table without keying.
+     * Table input requires an explicit key selector.
+     *
+     * <p>Remote execution rejects this overload. Use {@link #fromTable(Table, 
KeySelector)} to
+     * choose the key used for the agent's short-term memory.
      *
      * @param input Table to be processed by agents.
      * @return AgentBuilder for configuring the agent pipeline.
+     * @throws IllegalArgumentException in remote execution because a key 
selector is required.
      */
     public AgentBuilder fromTable(Table input) {
         return fromTable(input, null);
diff --git a/docs/content/docs/development/integrate_with_flink.md 
b/docs/content/docs/development/integrate_with_flink.md
index 1bea16897..261467e21 100644
--- a/docs/content/docs/development/integrate_with_flink.md
+++ b/docs/content/docs/development/integrate_with_flink.md
@@ -119,7 +119,14 @@ outputStream.print();
 
 {{< /tabs >}}
 
-The input `DataStream` must be `KeyedStream`, or user should provide 
`KeySelector` to tell how to convert the input `DataStream` to `KeyedStream`.
+The input `DataStream` must be keyed. Key selection follows the same rules in 
the Java and
+Python APIs:
+
+- If an explicit `KeySelector` is provided, it is always used, even when the 
input is
+  already keyed.
+- Otherwise, a `KeyedStream` input keeps its existing key.
+- If neither is available, the call fails with a clear error instead of 
silently keying
+  by the whole record.
 
 For complete, runnable examples, see {{< gh_link 
file="examples/src/main/java/org/apache/flink/agents/examples/WorkflowSingleAgentExample.java"
 name="`WorkflowSingleAgentExample.java`" >}} (Java) and {{< gh_link 
file="python/flink_agents/examples/quickstart/workflow_single_agent_example.py" 
name="`workflow_single_agent_example.py`" >}} (Python).
 
@@ -162,6 +169,10 @@ delete earlier rows. Both `from_table()` and `fromTable()` 
reject Tables that pr
 updates or deletes, such as a continuous `GROUP BY` aggregation whose results 
change
 as new records arrive.
 
+Both Java and Python require an explicit key selector for Table input. The 
selector
+extracts a business key from each row; rows with the same key share the Agent's
+short-term memory.
+
 {{< tabs "From/To Table" >}}
 
 {{< tab "Python" >}}
diff --git 
a/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md 
b/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md
index 691aab0dd..220a737a1 100644
--- a/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md
+++ b/docs/content/docs/get-started/quickstart/create-agent/multiple-agents.md
@@ -162,7 +162,7 @@ The Java table declares only the `id` and `review` columns.
 A Table row reaches the Action as a `pyflink.common.Row` in Python and an
 `org.apache.flink.types.Row` in Java. In Python, access fields with 
`row["id"]` or call
 `row.as_dict()` when dictionary methods such as `.get()` are needed. Build 
Your First Agent
-gave its Agent a `ProductReview` object in Python and a JSON string in Java, 
so this example
+gave its Agent a `ProductReview` object in both languages, so this example
 defines a second Agent class, `TableReviewAnalysisAgent`, for the row input. 
It redeclares the
 prompt, tool, and chat model setup that `ReviewAnalysisAgent` declares, and 
`ReviewAnalysisAgent`
 itself is unchanged. The key selector that reads `id` out of a row travels 
with this Agent, and
@@ -275,8 +275,8 @@ See [Integrate with Flink]({{< ref 
"docs/development/integrate_with_flink#fromto
 One review is too little to suggest an improvement, so the job groups the 
per-review analyses by
 product id in a one-minute tumbling window and reduces each group to a 
`ProductReviewSummary`. That
 type is declared in the shared types file and carries the product id, the 
score distribution, and
-the collected unsatisfied reasons. Python returns the summary object, and Java 
emits it as JSON
-text, because the next stage reads it back with Jackson. Scores outside 1–5 
are excluded from
+the collected unsatisfied reasons. Both Python and Java pass the summary 
object directly to
+the next Agent, keyed by product id. Scores outside 1–5 are excluded from
 the distribution; if none are valid, all five percentages are `0.0%`. The 
reasons are still retained.
 
 {{< tabs "Aggregate in a Window" >}}
@@ -325,15 +325,14 @@ aggregated_analysis_res_stream = (
 {{< tab "Java" >}}
 ```java
 static class AggregateScoreDistributionAndDislikeReasons
-        extends ProcessWindowFunction<ProductReviewAnalysisRes, String, 
String, TimeWindow> {
+        extends ProcessWindowFunction<ProductReviewAnalysisRes, 
ProductReviewSummary, String, TimeWindow> {
 
     @Override
     public void process(
             String key,
             Context context,
             Iterable<ProductReviewAnalysisRes> elements,
-            Collector<String> out)
-            throws JsonProcessingException {
+            Collector<ProductReviewSummary> out) {
 
         // Initialize rating counts for scores 1-5
         int[] ratingCounts = new int[5];
@@ -371,11 +370,11 @@ static class AggregateScoreDistributionAndDislikeReasons
         // Create and emit the aggregated result
         ProductReviewSummary summary =
                 new ProductReviewSummary(key, formattedPercentages, 
reasonList);
-        out.collect(MAPPER.writeValueAsString(summary));
+        out.collect(summary);
     }
 }
 
-DataStream<String> aggregatedAnalysisResStream =
+DataStream<ProductReviewSummary> aggregatedAnalysisResStream =
         reviewAnalysisResStream
                 .keyBy(ProductReviewAnalysisRes::getId)
                 
.window(TumblingProcessingTimeWindows.of(Duration.ofMinutes(1)))
@@ -454,10 +453,8 @@ public class ProductSuggestionAgent extends Agent {
     /** Process input event. */
     @Action(EventType.InputEvent)
     public static void processInput(Event event, RunnerContext ctx) throws 
Exception {
-        // The window stage emitted JSON text, so this Agent reads it back 
first.
         InputEvent inputEvent = (InputEvent) event;
-        String input = (String) inputEvent.getInput();
-        ProductReviewSummary summary = MAPPER.readValue(input, 
ProductReviewSummary.class);
+        ProductReviewSummary summary = (ProductReviewSummary) 
inputEvent.getInput();
         // Rest of the body keeps the id and score histogram in short-term 
memory and
         // sends ChatRequestEvent for generateSuggestionModel.
         ...
@@ -495,7 +492,7 @@ agents_env.execute("Workflow Multiple Agent Example Job")
 ```java
 DataStream<Object> productSuggestionResStream =
         agentsEnv
-                .fromDataStream(aggregatedAnalysisResStream)
+                .fromDataStream(aggregatedAnalysisResStream, 
ProductReviewSummary::getId)
                 .apply(new ProductSuggestionAgent())
                 .toDataStream();
 
diff --git 
a/docs/content/docs/get-started/quickstart/create-agent/single-agent.md 
b/docs/content/docs/get-started/quickstart/create-agent/single-agent.md
index 24af388fa..3e305d6a7 100644
--- a/docs/content/docs/get-started/quickstart/create-agent/single-agent.md
+++ b/docs/content/docs/get-started/quickstart/create-agent/single-agent.md
@@ -263,17 +263,12 @@ public static void notifyShippingManager(
 @Action(EventType.InputEvent)
 public static void processInput(Event event, RunnerContext ctx) throws 
Exception {
     InputEvent inputEvent = (InputEvent) event;
-    String input = (String) inputEvent.getInput();
-    MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
     CustomTypesAndResources.ProductReview inputObj =
-            MAPPER.readValue(input, 
CustomTypesAndResources.ProductReview.class);
+            (CustomTypesAndResources.ProductReview) inputEvent.getInput();
 
     ctx.getShortTermMemory().set("id", inputObj.getId());
 
-    String content =
-            String.format(
-                    "{\n" + "\"id\": %s,\n" + "\"review\": \"%s\"\n" + "}",
-                    inputObj.getId(), inputObj.getReview());
+    String content = MAPPER.writeValueAsString(inputObj);
     ChatMessage msg = new ChatMessage(MessageRole.USER, "");
 
     ctx.sendEvent(
@@ -363,22 +358,28 @@ agents_env.execute("Workflow Agent Example Job")
 
 {{< tab "Java" >}}
 ```java
+// Configure this mapper once on the enclosing example class:
+// private static final ObjectMapper MAPPER = new ObjectMapper()
+//         .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+// ProductReview is CustomTypesAndResources.ProductReview.
 // Read product reviews from input_data.txt file as a streaming source.
 // Each element represents a ProductReview.
 File inputDataFile = copyResource("input_data.txt");
-DataStream<String> productReviewStream =
+DataStream<ProductReview> productReviewStream =
         env.fromSource(
-                FileSource.forRecordStreamFormat(
-                                new TextLineInputFormat(),
-                                new Path(inputDataFile.getAbsolutePath()))
-                        .build(),
-                WatermarkStrategy.noWatermarks(),
-                "streaming-agent-example");
+                        FileSource.forRecordStreamFormat(
+                                        new TextLineInputFormat(),
+                                        new 
Path(inputDataFile.getAbsolutePath()))
+                                .build(),
+                        WatermarkStrategy.noWatermarks(),
+                        "streaming-agent-example")
+                .map(json -> MAPPER.readValue(json, ProductReview.class))
+                .returns(ProductReview.class);
 
 // Use the ReviewAnalysisAgent to analyze each product review.
 DataStream<Object> reviewAnalysisResStream =
         agentsEnv
-                .fromDataStream(productReviewStream)
+                .fromDataStream(productReviewStream, ProductReview::getId)
                 .apply(new ReviewAnalysisAgent())
                 .toDataStream();
 
diff --git a/docs/content/docs/get-started/quickstart/react_agent.md 
b/docs/content/docs/get-started/quickstart/react_agent.md
index 325baae91..6d271d02a 100644
--- a/docs/content/docs/get-started/quickstart/react_agent.md
+++ b/docs/content/docs/get-started/quickstart/react_agent.md
@@ -255,7 +255,7 @@ DataStream<Row> productReviewStream =
 // record shipping question.
 DataStream<Object> reviewAnalysisResStream =
         agentsEnv
-                .fromDataStream(productReviewStream)
+                .fromDataStream(productReviewStream, row -> (String) 
row.getField("id"))
                 .apply(reviewAnalysisReactAgent)
                 .toDataStream();
 
diff --git a/docs/content/docs/get-started/quickstart/yaml_agent.md 
b/docs/content/docs/get-started/quickstart/yaml_agent.md
index 6f041eb92..9a6c3ecaa 100644
--- a/docs/content/docs/get-started/quickstart/yaml_agent.md
+++ b/docs/content/docs/get-started/quickstart/yaml_agent.md
@@ -318,22 +318,28 @@ agents_env.execute("YAML Agent Example Job")
 File yamlFile = copyResource("yaml/yaml_review_analysis_agent.yaml");
 agentsEnv.loadYaml(yamlFile.toPath());
 
+// Configure this mapper once on the enclosing example class:
+// private static final ObjectMapper MAPPER = new ObjectMapper()
+//         .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+// ProductReview is CustomTypesAndResources.ProductReview.
 // Read product reviews from input_data.txt file as a streaming source.
 File inputDataFile = copyResource("input_data.txt");
-DataStream<String> productReviewStream =
+DataStream<ProductReview> productReviewStream =
         env.fromSource(
-                FileSource.forRecordStreamFormat(
-                                new TextLineInputFormat(),
-                                new Path(inputDataFile.getAbsolutePath()))
-                        .build(),
-                WatermarkStrategy.noWatermarks(),
-                "yaml-review-analysis-example");
+                        FileSource.forRecordStreamFormat(
+                                        new TextLineInputFormat(),
+                                        new 
Path(inputDataFile.getAbsolutePath()))
+                                .build(),
+                        WatermarkStrategy.noWatermarks(),
+                        "yaml-review-analysis-example")
+                .map(json -> MAPPER.readValue(json, ProductReview.class))
+                .returns(ProductReview.class);
 
 // Apply the YAML-declared agent BY NAME — `apply` accepts either an
 // Agent instance or the name of an agent registered on the environment.
 DataStream<Object> reviewAnalysisResStream =
         agentsEnv
-                .fromDataStream(productReviewStream)
+                .fromDataStream(productReviewStream, ProductReview::getId)
                 .apply("review_analysis_agent")
                 .toDataStream();
 
diff --git 
a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationTest.java
 
b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationTest.java
index d7b34970a..edb51d0cc 100644
--- 
a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationTest.java
+++ 
b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationTest.java
@@ -193,7 +193,7 @@ public class FlinkIntegrationTest {
 
         DataStream<FlinkIntegrationAgent.TypedOutput> outputStream =
                 agentsEnv
-                        .fromDataStream(inputStream)
+                        .fromDataStream(inputStream, value -> value)
                         .apply(new FlinkIntegrationAgent.TypedOutputAgent())
                         
.toDataStream(TypeInformation.of(FlinkIntegrationAgent.TypedOutput.class));
 
@@ -228,7 +228,7 @@ public class FlinkIntegrationTest {
 
         Table outputTable =
                 agentsEnv
-                        .fromDataStream(inputStream)
+                        .fromDataStream(inputStream, value -> value)
                         .apply(new FlinkIntegrationAgent.TypedOutputAgent())
                         
.toTable(TypeInformation.of(FlinkIntegrationAgent.TypedOutput.class));
 
@@ -260,7 +260,7 @@ public class FlinkIntegrationTest {
 
         Table outputTable =
                 agentsEnv
-                        .fromDataStream(inputStream)
+                        .fromDataStream(inputStream, value -> value)
                         .apply(new FlinkIntegrationAgent.TypedOutputAgent())
                         .toTable(outputSchema);
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingExample.java
index b33dd2d3d..6ab76c87e 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingExample.java
@@ -94,7 +94,7 @@ public class ModelRoutingExample {
 
         DataStream<Object> resultStream =
                 agentsEnv
-                        .fromDataStream(requestStream)
+                        .fromDataStream(requestStream, value -> value)
                         .apply(new ModelRoutingAgent())
                         .toDataStream();
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingJudgeExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingJudgeExample.java
index 9e2a26c0b..fbc5f7313 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingJudgeExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/ModelRoutingJudgeExample.java
@@ -96,7 +96,7 @@ public class ModelRoutingJudgeExample {
 
         DataStream<Object> resultStream =
                 agentsEnv
-                        .fromDataStream(requestStream)
+                        .fromDataStream(requestStream, value -> value)
                         .apply(new ModelRoutingAgent())
                         .toDataStream();
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/ReActAgentExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/ReActAgentExample.java
index fef67f32a..f82e663ce 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/ReActAgentExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/ReActAgentExample.java
@@ -134,7 +134,7 @@ public class ReActAgentExample {
         // record shipping question.
         DataStream<Object> reviewAnalysisResStream =
                 agentsEnv
-                        .fromDataStream(productReviewStream)
+                        .fromDataStream(productReviewStream, row -> (String) 
row.getField("id"))
                         .apply(reviewAnalysisReactAgent)
                         .toDataStream();
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/SkillsAgentExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/SkillsAgentExample.java
index 6ca22c6bf..e3758674b 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/SkillsAgentExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/SkillsAgentExample.java
@@ -60,7 +60,10 @@ public class SkillsAgentExample {
 
         // Use the MathAgent to answer each question with the math-calculator 
skill.
         DataStream<Object> answerStream =
-                agentsEnv.fromDataStream(questionStream).apply(new 
MathAgent()).toDataStream();
+                agentsEnv
+                        .fromDataStream(questionStream, value -> value)
+                        .apply(new MathAgent())
+                        .toDataStream();
 
         // Print the answers to stdout.
         answerStream.print();
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/WorkflowMultipleAgentExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/WorkflowMultipleAgentExample.java
index 7218faa2d..c27639687 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/WorkflowMultipleAgentExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/WorkflowMultipleAgentExample.java
@@ -18,8 +18,6 @@
 
 package org.apache.flink.agents.examples;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.flink.agents.api.AgentsExecutionEnvironment;
 import org.apache.flink.agents.api.agents.AgentExecutionOptions;
 import org.apache.flink.agents.api.resource.ResourceType;
@@ -65,8 +63,6 @@ import static 
org.apache.flink.agents.examples.agents.CustomTypesAndResources.Pr
  */
 public class WorkflowMultipleAgentExample {
 
-    private static final ObjectMapper MAPPER = new ObjectMapper();
-
     /**
      * ProcessWindowFunction to aggregate score distribution and dislike 
reasons.
      *
@@ -74,15 +70,15 @@ public class WorkflowMultipleAgentExample {
      * calculating score distribution percentages and collecting all 
unsatisfied reasons.
      */
     static class AggregateScoreDistributionAndDislikeReasons
-            extends ProcessWindowFunction<ProductReviewAnalysisRes, String, 
String, TimeWindow> {
+            extends ProcessWindowFunction<
+                    ProductReviewAnalysisRes, ProductReviewSummary, String, 
TimeWindow> {
 
         @Override
         public void process(
                 String key,
                 Context context,
                 Iterable<ProductReviewAnalysisRes> elements,
-                Collector<String> out)
-                throws JsonProcessingException {
+                Collector<ProductReviewSummary> out) {
 
             // Initialize rating counts for scores 1-5
             int[] ratingCounts = new int[5];
@@ -120,7 +116,7 @@ public class WorkflowMultipleAgentExample {
             // Create and emit the aggregated result
             ProductReviewSummary summary =
                     new ProductReviewSummary(key, formattedPercentages, 
reasonList);
-            out.collect(MAPPER.writeValueAsString(summary));
+            out.collect(summary);
         }
     }
 
@@ -179,7 +175,7 @@ public class WorkflowMultipleAgentExample {
         // Aggregate the analysis results in 1-minute tumbling windows.
         // This produces a score distribution and collects all unsatisfied 
reasons for
         // each product.
-        DataStream<String> aggregatedAnalysisResStream =
+        DataStream<ProductReviewSummary> aggregatedAnalysisResStream =
                 reviewAnalysisResStream
                         .keyBy(ProductReviewAnalysisRes::getId)
                         
.window(TumblingProcessingTimeWindows.of(Duration.ofMinutes(1)))
@@ -189,7 +185,7 @@ public class WorkflowMultipleAgentExample {
         // suggestions based on the aggregated analysis results.
         DataStream<Object> productSuggestionResStream =
                 agentsEnv
-                        .fromDataStream(aggregatedAnalysisResStream)
+                        .fromDataStream(aggregatedAnalysisResStream, 
ProductReviewSummary::getId)
                         .apply(new ProductSuggestionAgent())
                         .toDataStream();
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/WorkflowSingleAgentExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/WorkflowSingleAgentExample.java
index 62c317fe0..ddc371f67 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/WorkflowSingleAgentExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/WorkflowSingleAgentExample.java
@@ -17,10 +17,13 @@
  */
 package org.apache.flink.agents.examples;
 
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.flink.agents.api.AgentsExecutionEnvironment;
 import org.apache.flink.agents.api.agents.AgentExecutionOptions;
 import org.apache.flink.agents.api.resource.ResourceType;
 import org.apache.flink.agents.examples.agents.CustomTypesAndResources;
+import 
org.apache.flink.agents.examples.agents.CustomTypesAndResources.ProductReview;
 import org.apache.flink.agents.examples.agents.ReviewAnalysisAgent;
 import org.apache.flink.api.common.eventtime.WatermarkStrategy;
 import org.apache.flink.connector.file.src.FileSource;
@@ -48,6 +51,9 @@ import java.nio.file.StandardCopyOption;
  */
 public class WorkflowSingleAgentExample {
 
+    private static final ObjectMapper MAPPER =
+            new 
ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+
     /** Runs the example pipeline. */
     public static void main(String[] args) throws Exception {
         // Set up the Flink streaming environment and the Agents execution 
environment.
@@ -68,19 +74,21 @@ public class WorkflowSingleAgentExample {
         // Read product reviews from input_data.txt file as a streaming source.
         // Each element represents a ProductReview.
         File inputDataFile = copyResource("input_data.txt");
-        DataStream<String> productReviewStream =
+        DataStream<ProductReview> productReviewStream =
                 env.fromSource(
-                        FileSource.forRecordStreamFormat(
-                                        new TextLineInputFormat(),
-                                        new 
Path(inputDataFile.getAbsolutePath()))
-                                .build(),
-                        WatermarkStrategy.noWatermarks(),
-                        "streaming-agent-example");
+                                FileSource.forRecordStreamFormat(
+                                                new TextLineInputFormat(),
+                                                new 
Path(inputDataFile.getAbsolutePath()))
+                                        .build(),
+                                WatermarkStrategy.noWatermarks(),
+                                "streaming-agent-example")
+                        .map(json -> MAPPER.readValue(json, 
ProductReview.class))
+                        .returns(ProductReview.class);
 
         // Use the ReviewAnalysisAgent to analyze each product review.
         DataStream<Object> reviewAnalysisResStream =
                 agentsEnv
-                        .fromDataStream(productReviewStream)
+                        .fromDataStream(productReviewStream, 
ProductReview::getId)
                         .apply(new ReviewAnalysisAgent())
                         .toDataStream();
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/YamlWorkflowAgentExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/YamlWorkflowAgentExample.java
index b425ccd91..6a7761450 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/YamlWorkflowAgentExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/YamlWorkflowAgentExample.java
@@ -17,8 +17,11 @@
  */
 package org.apache.flink.agents.examples;
 
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.flink.agents.api.AgentsExecutionEnvironment;
 import org.apache.flink.agents.api.agents.AgentExecutionOptions;
+import 
org.apache.flink.agents.examples.agents.CustomTypesAndResources.ProductReview;
 import org.apache.flink.api.common.eventtime.WatermarkStrategy;
 import org.apache.flink.connector.file.src.FileSource;
 import org.apache.flink.connector.file.src.reader.TextLineInputFormat;
@@ -47,6 +50,9 @@ import static 
org.apache.flink.agents.examples.WorkflowSingleAgentExample.copyRe
  */
 public class YamlWorkflowAgentExample {
 
+    private static final ObjectMapper MAPPER =
+            new 
ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+
     /** Runs the example pipeline. */
     public static void main(String[] args) throws Exception {
         // Set up the Flink streaming environment and the Agents execution 
environment.
@@ -67,20 +73,22 @@ public class YamlWorkflowAgentExample {
         // Read product reviews from input_data.txt file as a streaming source.
         // Each element represents a ProductReview.
         File inputDataFile = copyResource("input_data.txt");
-        DataStream<String> productReviewStream =
+        DataStream<ProductReview> productReviewStream =
                 env.fromSource(
-                        FileSource.forRecordStreamFormat(
-                                        new TextLineInputFormat(),
-                                        new 
Path(inputDataFile.getAbsolutePath()))
-                                .build(),
-                        WatermarkStrategy.noWatermarks(),
-                        "yaml-review-analysis-example");
+                                FileSource.forRecordStreamFormat(
+                                                new TextLineInputFormat(),
+                                                new 
Path(inputDataFile.getAbsolutePath()))
+                                        .build(),
+                                WatermarkStrategy.noWatermarks(),
+                                "yaml-review-analysis-example")
+                        .map(json -> MAPPER.readValue(json, 
ProductReview.class))
+                        .returns(ProductReview.class);
 
         // Apply the YAML-declared agent BY NAME — ``apply`` accepts either an
         // Agent instance or the name of an agent registered on the 
environment.
         DataStream<Object> reviewAnalysisResStream =
                 agentsEnv
-                        .fromDataStream(productReviewStream)
+                        .fromDataStream(productReviewStream, 
ProductReview::getId)
                         .apply("review_analysis_agent")
                         .toDataStream();
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java
index 959ad9cfe..1c773cc07 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java
@@ -76,17 +76,17 @@ public class ProductSuggestionAgent extends Agent {
     @Action(EventType.InputEvent)
     public static void processInput(Event event, RunnerContext ctx) throws 
Exception {
         InputEvent inputEvent = (InputEvent) event;
-        String input = (String) inputEvent.getInput();
-
-        ProductReviewSummary summary = MAPPER.readValue(input, 
ProductReviewSummary.class);
+        ProductReviewSummary summary = (ProductReviewSummary) 
inputEvent.getInput();
 
         ctx.getShortTermMemory().set(ID, summary.getId());
         ctx.getShortTermMemory().set(SCORE_HIST, summary.getScoreHist());
 
         String content =
-                String.format(
-                        "{\n\"id\": %s,\n\"score_histogram\": 
%s,\n\"unsatisfied_reasons\": %s\n}",
-                        summary.getId(), summary.getScoreHist(), 
summary.getUnsatisfiedReasons());
+                MAPPER.writeValueAsString(
+                        Map.of(
+                                "id", summary.getId(),
+                                "score_histogram", summary.getScoreHist(),
+                                "unsatisfied_reasons", 
summary.getUnsatisfiedReasons()));
 
         ChatMessage msg = new ChatMessage(MessageRole.USER, "");
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java
index 46e30079a..4e78da8a7 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java
@@ -17,7 +17,6 @@
  */
 package org.apache.flink.agents.examples.agents;
 
-import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.flink.agents.api.Event;
@@ -91,17 +90,12 @@ public class ReviewAnalysisAgent extends Agent {
     @Action(EventType.InputEvent)
     public static void processInput(Event event, RunnerContext ctx) throws 
Exception {
         InputEvent inputEvent = (InputEvent) event;
-        String input = (String) inputEvent.getInput();
-        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
         CustomTypesAndResources.ProductReview inputObj =
-                MAPPER.readValue(input, 
CustomTypesAndResources.ProductReview.class);
+                (CustomTypesAndResources.ProductReview) inputEvent.getInput();
 
         ctx.getShortTermMemory().set("id", inputObj.getId());
 
-        String content =
-                String.format(
-                        "{\n" + "\"id\": %s,\n" + "\"review\": \"%s\"\n" + "}",
-                        inputObj.getId(), inputObj.getReview());
+        String content = MAPPER.writeValueAsString(inputObj);
         ChatMessage msg = new ChatMessage(MessageRole.USER, "");
 
         ctx.sendEvent(
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java
index ee9bef4e9..1c41bb62a 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java
@@ -54,7 +54,7 @@ import static 
org.apache.flink.agents.examples.agents.CustomTypesAndResources.RE
  * score and reasons for dissatisfaction.
  *
  * <p>The main difference from {@link ReviewAnalysisAgent} is that this agent 
handles {@link Row}
- * input instead of JSON strings.
+ * input instead of {@link CustomTypesAndResources.ProductReview} objects.
  */
 public class TableReviewAnalysisAgent extends Agent {
 
diff --git 
a/examples/src/main/java/org/apache/flink/agents/examples/openai/OpenAiModelRoutingExample.java
 
b/examples/src/main/java/org/apache/flink/agents/examples/openai/OpenAiModelRoutingExample.java
index 965d435d3..bb5f23d11 100644
--- 
a/examples/src/main/java/org/apache/flink/agents/examples/openai/OpenAiModelRoutingExample.java
+++ 
b/examples/src/main/java/org/apache/flink/agents/examples/openai/OpenAiModelRoutingExample.java
@@ -99,7 +99,7 @@ public class OpenAiModelRoutingExample {
 
         DataStream<Object> resultStream =
                 agentsEnv
-                        .fromDataStream(requestStream)
+                        .fromDataStream(requestStream, value -> value)
                         .apply(new ModelRoutingAgent())
                         .toDataStream();
 
diff --git a/python/flink_agents/api/execution_environment.py 
b/python/flink_agents/api/execution_environment.py
index d368fd9a6..96a07186b 100644
--- a/python/flink_agents/api/execution_environment.py
+++ b/python/flink_agents/api/execution_environment.py
@@ -222,10 +222,9 @@ class AgentsExecutionEnvironment(ABC):
         ----------
         input : Table
             Receive a Table as input.
-        t_env: StreamTableEnvironment
-            table environment supports convert Table to/from DataStream.
         key_selector : KeySelector
-            Extract key from each input record.
+            Required selector extracting the business key from each table row.
+            Rows with the same key share the agent's short-term memory.
 
         Returns:
         -------
diff --git a/python/flink_agents/runtime/remote_execution_environment.py 
b/python/flink_agents/runtime/remote_execution_environment.py
index 8f9e35aab..6b5478421 100644
--- a/python/flink_agents/runtime/remote_execution_environment.py
+++ b/python/flink_agents/runtime/remote_execution_environment.py
@@ -360,14 +360,17 @@ class 
RemoteExecutionEnvironment(AgentsExecutionEnvironment):
     def __process_input_datastream(
         input: DataStream, key_selector: KeySelector | Callable | None = None
     ) -> KeyedStream:
+        if key_selector is not None:
+            # An explicit selector always wins, even when the input is already 
keyed,
+            # so callers can re-key a keyed input without re-building the 
pipeline.
+            return input.key_by(key_selector)
         if isinstance(input, KeyedStream):
             return input
-        else:
-            if key_selector is None:
-                msg = "KeySelector must be provided."
-                raise RuntimeError(msg)
-            input = input.key_by(key_selector)
-            return input
+        msg = (
+            "A key selector must be provided when the input DataStream is not 
keyed; "
+            "pass a KeyedStream or a key selector."
+        )
+        raise RuntimeError(msg)
 
     def from_datastream(
         self, input: DataStream, key_selector: KeySelector | Callable | None = 
None
@@ -379,8 +382,10 @@ class 
RemoteExecutionEnvironment(AgentsExecutionEnvironment):
         input : DataStream
             Receive a DataStream as input.
         key_selector : KeySelector
-            Extract key from each input record, must not be None when input is
-            not KeyedStream.
+            Extract key from each input record. An explicit selector always 
takes
+            precedence, even over the key of an already-keyed input; otherwise 
a
+            KeyedStream input keeps its key, and a non-keyed input without a
+            selector is rejected.
         """
         input = self.__process_input_datastream(input, key_selector)
 
@@ -404,8 +409,16 @@ class 
RemoteExecutionEnvironment(AgentsExecutionEnvironment):
         input : Table
             Receive a Table as input.
         key_selector : KeySelector
-            Extract key from each input record.
+            Required selector extracting the business key from each table row.
+            Rows with the same key share the agent's short-term memory.
         """
+        if key_selector is None:
+            msg = (
+                "A key selector must be provided for Table input; "
+                "pass a key selector to from_table."
+            )
+            raise RuntimeError(msg)
+
         input = self.t_env.to_data_stream(table=input)
 
         input = input.map(lambda x: x, output_type=PickledBytesTypeInfo())
diff --git 
a/python/flink_agents/runtime/tests/test_remote_execution_environment.py 
b/python/flink_agents/runtime/tests/test_remote_execution_environment.py
index 2f9b6f88a..6584b733f 100644
--- a/python/flink_agents/runtime/tests/test_remote_execution_environment.py
+++ b/python/flink_agents/runtime/tests/test_remote_execution_environment.py
@@ -28,7 +28,7 @@ import pytest
 import yaml
 from pyflink.common import Row
 from pyflink.common.typeinfo import ExternalTypeInfo, Types
-from pyflink.datastream import StreamExecutionEnvironment
+from pyflink.datastream import KeyedStream, StreamExecutionEnvironment
 from pyflink.table import DataTypes, Schema, StreamTableEnvironment
 
 from flink_agents.api.agents.agent import Agent
@@ -665,3 +665,51 @@ def 
test_datastream_to_table_uses_input_environment(provide_table_env, use_schem
     assert 
output.get_execution_environment()._j_stream_execution_environment.equals(
         env._j_stream_execution_environment
     )
+
+
+def test_key_selection_combinations() -> None:
+    """Key selection follows the same rules as the Java runtime.
+
+    An explicit selector always wins (even over the key of an already-keyed
+    input), a keyed input without a selector keeps its key, and an unkeyed
+    input without a selector is rejected with a clear error instead of being
+    silently keyed by the whole record.
+    """
+    env = StreamExecutionEnvironment.get_execution_environment()
+    unkeyed = env.from_collection(["same:first", "other:first"])
+    keyed = unkeyed.key_by(lambda value: value.split(":")[0])
+    process = (
+        
RemoteExecutionEnvironment._RemoteExecutionEnvironment__process_input_datastream
+    )
+
+    def session(value: str) -> str:
+        return value.split(":")[0]
+
+    # An explicit selector wins even on a keyed input: the pipeline is re-keyed
+    # (a new stream) rather than silently keeping the input's own key.
+    assert process(keyed, session) is not keyed
+    # A keyed input without a selector keeps its key.
+    assert process(keyed, None) is keyed
+    # An unkeyed input with a selector is keyed by that selector.
+    assert isinstance(process(unkeyed, session), KeyedStream)
+    # An unkeyed input without a selector is rejected with a clear error.
+    with pytest.raises(RuntimeError, match="key selector"):
+        process(unkeyed, None)
+
+
[email protected]("provide_selector", [False, True])
+def test_table_requires_explicit_key_selector(provide_selector: bool) -> None:
+    """Table input must be keyed explicitly, including when None is passed."""
+    env = StreamExecutionEnvironment.get_execution_environment()
+    t_env = StreamTableEnvironment.create(env)
+    remote_env = RemoteExecutionEnvironment(env=env, t_env=t_env)
+    table = t_env.from_elements([("same", "first"), ("same", "second")], 
["id", "text"])
+
+    if provide_selector:
+        builder = remote_env.from_table(table, key_selector=lambda row: row[0])
+        assert isinstance(builder._RemoteAgentBuilder__input, KeyedStream)
+    else:
+        with pytest.raises(RuntimeError, match="key selector.*Table input"):
+            remote_env.from_table(table)
+        with pytest.raises(RuntimeError, match="key selector.*Table input"):
+            remote_env.from_table(table, key_selector=None)
diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironment.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironment.java
index 8cb9e599d..e2218f665 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironment.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironment.java
@@ -33,6 +33,7 @@ import org.apache.flink.api.java.typeutils.RowTypeInfo;
 import org.apache.flink.configuration.ConfigConstants;
 import org.apache.flink.configuration.YamlParserUtils;
 import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.KeyedStream;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
 import org.apache.flink.table.api.Schema;
 import org.apache.flink.table.api.Table;
@@ -94,6 +95,10 @@ public class RemoteExecutionEnvironment extends 
AgentsExecutionEnvironment {
 
     @Override
     public <K> AgentBuilder fromTable(Table input, KeySelector<Object, K> 
keySelector) {
+        if (keySelector == null) {
+            throw new IllegalArgumentException(
+                    "A key selector must be provided for Table input; pass a 
key selector to fromTable.");
+        }
         return new RemoteAgentBuilder<>(
                 input, getTableEnvironment(), keySelector, env, config, 
resources, agents);
     }
@@ -220,6 +225,7 @@ public class RemoteExecutionEnvironment extends 
AgentsExecutionEnvironment {
         }
 
         @Override
+        @SuppressWarnings("unchecked")
         public DataStream<Object> toDataStream() {
             if (agentPlan == null) {
                 throw new IllegalStateException("Must apply agent before 
calling toDataStream");
@@ -229,10 +235,21 @@ public class RemoteExecutionEnvironment extends 
AgentsExecutionEnvironment {
                 if (keySelector != null) {
                     outputDataStream =
                             CompileUtils.connectToAgent(inputDataStream, 
keySelector, agentPlan);
-                } else {
-                    // If no key selector provided, use a simple pass-through 
key selector
+                } else if (inputDataStream instanceof KeyedStream) {
+                    // An already-keyed input carries its business key; 
connect through the
+                    // keyed-stream path to preserve that key, instead of 
replacing it with a
+                    // pass-through selector that would split a session across 
its messages.
                     outputDataStream =
-                            CompileUtils.connectToAgent(inputDataStream, x -> 
x, agentPlan);
+                            CompileUtils.connectToAgent(
+                                    (KeyedStream<T, ?>) inputDataStream, 
agentPlan);
+                } else {
+                    // Without a key selector there is no business key to key 
the agent by;
+                    // keying by the whole record would silently split 
sessions, so require
+                    // the caller to pass a keyed stream or a selector, as 
documented.
+                    throw new IllegalArgumentException(
+                            "A key selector must be provided when the input 
DataStream is not"
+                                    + " keyed; pass a KeyedStream or a key 
selector to"
+                                    + " fromDataStream.");
                 }
             }
 
diff --git 
a/runtime/src/test/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironmentTest.java
 
b/runtime/src/test/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironmentTest.java
index 0bb09df6f..d49e291f8 100644
--- 
a/runtime/src/test/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironmentTest.java
+++ 
b/runtime/src/test/java/org/apache/flink/agents/runtime/env/RemoteExecutionEnvironmentTest.java
@@ -18,15 +18,32 @@
 package org.apache.flink.agents.runtime.env;
 
 import org.apache.flink.agents.api.AgentsExecutionEnvironment;
+import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
+import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.OutputEvent;
+import org.apache.flink.agents.api.agents.Agent;
+import org.apache.flink.agents.api.annotation.Action;
+import org.apache.flink.agents.api.context.MemoryObject;
+import org.apache.flink.agents.api.context.RunnerContext;
 import org.apache.flink.agents.plan.AgentConfiguration;
 import org.apache.flink.api.common.JobExecutionResult;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.datastream.KeyedStream;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.api.Table;
+import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.CloseableIterator;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.UUID;
 
 import static 
org.apache.flink.agents.runtime.env.RemoteExecutionEnvironment.FLINK_CONF_FILENAME;
@@ -95,4 +112,158 @@ public class RemoteExecutionEnvironmentTest {
 
         verify(flinkEnv).execute(customJobName);
     }
+
+    @Test
+    void fromDataStreamPreservesExistingKeyedStreamKeyWhenNoSelectorGiven() 
throws Exception {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+        AgentsExecutionEnvironment agentsEnv =
+                AgentsExecutionEnvironment.getExecutionEnvironment(env);
+
+        KeyedStream<String, String> keyedBySession =
+                env.fromData("same:first", "same:second", "same:first again", 
"other:first")
+                        .keyBy(value -> value.split(":")[0]);
+
+        DataStream<Object> output =
+                agentsEnv.fromDataStream(keyedBySession).apply(new 
CountingAgent()).toDataStream();
+
+        List<Object> results = new ArrayList<>();
+        try (CloseableIterator<Object> iterator = output.executeAndCollect()) {
+            iterator.forEachRemaining(results::add);
+        }
+
+        // Messages of the "same" session share short-term memory and count 1, 
2, 3 in their
+        // arrival order, while the independent "other" session starts from 1. 
A pass-through
+        // selector that replaces the session key with the whole message would 
count every
+        // "same" message as 1.
+        assertThat(results).containsExactlyInAnyOrder("same:1", "same:2", 
"same:3", "other:1");
+    }
+
+    @Test
+    void fromDataStreamUsesExplicitSelectorEvenOnKeyedInput() throws Exception 
{
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+        AgentsExecutionEnvironment agentsEnv =
+                AgentsExecutionEnvironment.getExecutionEnvironment(env);
+
+        // The stream is keyed by the whole record; the explicit selector keys 
by session, and
+        // the selector must win: the "same" messages share short-term memory 
either way only
+        // if the session key is applied rather than the stream's own 
whole-record key.
+        KeyedStream<String, String> keyedByWholeRecord =
+                env.fromData("same:first", "same:second", 
"other:first").keyBy(value -> value);
+
+        DataStream<Object> output =
+                agentsEnv
+                        .fromDataStream(keyedByWholeRecord, value -> 
value.split(":")[0])
+                        .apply(new CountingAgent())
+                        .toDataStream();
+
+        List<Object> results = new ArrayList<>();
+        try (CloseableIterator<Object> iterator = output.executeAndCollect()) {
+            iterator.forEachRemaining(results::add);
+        }
+
+        assertThat(results).containsExactlyInAnyOrder("same:1", "same:2", 
"other:1");
+    }
+
+    @Test
+    void fromDataStreamKeysUnkeyedInputBySelector() throws Exception {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+        AgentsExecutionEnvironment agentsEnv =
+                AgentsExecutionEnvironment.getExecutionEnvironment(env);
+
+        DataStream<String> unkeyed = env.fromData("same:first", "same:second", 
"other:first");
+
+        DataStream<Object> output =
+                agentsEnv
+                        .fromDataStream(unkeyed, value -> value.split(":")[0])
+                        .apply(new CountingAgent())
+                        .toDataStream();
+
+        List<Object> results = new ArrayList<>();
+        try (CloseableIterator<Object> iterator = output.executeAndCollect()) {
+            iterator.forEachRemaining(results::add);
+        }
+
+        assertThat(results).containsExactlyInAnyOrder("same:1", "same:2", 
"other:1");
+    }
+
+    @Test
+    void fromDataStreamRejectsUnkeyedInputWithoutSelector() {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+        AgentsExecutionEnvironment agentsEnv =
+                AgentsExecutionEnvironment.getExecutionEnvironment(env);
+
+        DataStream<String> unkeyed = env.fromData("same:first");
+
+        assertThatThrownBy(
+                        () ->
+                                agentsEnv
+                                        .fromDataStream(unkeyed)
+                                        .apply(new CountingAgent())
+                                        .toDataStream())
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("key selector must be provided");
+    }
+
+    @Test
+    void fromTableRejectsMissingSelector() {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
+        AgentsExecutionEnvironment agentsEnv =
+                AgentsExecutionEnvironment.getExecutionEnvironment(env, 
tableEnv);
+        Table input = tableEnv.fromValues(DataTypes.STRING(), "same:first");
+
+        assertThatThrownBy(() -> agentsEnv.fromTable(input))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("key selector")
+                .hasMessageContaining("Table input");
+        assertThatThrownBy(() -> agentsEnv.fromTable(input, null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("key selector")
+                .hasMessageContaining("Table input");
+    }
+
+    @Test
+    void fromTableUsesExplicitBusinessKey() throws Exception {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        env.setParallelism(1);
+        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
+        AgentsExecutionEnvironment agentsEnv =
+                AgentsExecutionEnvironment.getExecutionEnvironment(env, 
tableEnv);
+        Table input =
+                tableEnv.fromValues(DataTypes.STRING(), "same:first", 
"same:second", "other:first");
+
+        DataStream<Object> output =
+                agentsEnv
+                        .fromTable(input, row -> ((String) ((Row) 
row).getField(0)).split(":")[0])
+                        .apply(new CountingAgent())
+                        .toDataStream();
+
+        List<Object> results = new ArrayList<>();
+        try (CloseableIterator<Object> iterator = output.executeAndCollect()) {
+            iterator.forEachRemaining(results::add);
+        }
+        assertThat(results).containsExactlyInAnyOrder("same:1", "same:2", 
"other:1");
+    }
+
+    /** Model-free agent counting inputs per business key in short-term 
memory. */
+    public static class CountingAgent extends Agent {
+
+        @Action(EventType.InputEvent)
+        public static void countInputs(Event event, RunnerContext context) 
throws Exception {
+            Object value = InputEvent.fromEvent(event).getInput();
+            String input = (String) (value instanceof Row ? ((Row) 
value).getField(0) : value);
+            String session = input.split(":")[0];
+            MemoryObject memory = context.getShortTermMemory();
+            int count = 1;
+            if (memory.isExist("count")) {
+                count = ((Number) memory.get("count").getValue()).intValue() + 
1;
+            }
+            memory.set("count", count);
+            context.sendEvent(new OutputEvent(session + ":" + count));
+        }
+    }
 }
diff --git 
a/runtime/src/test/java/org/apache/flink/agents/runtime/env/TypedOutputTerminalTest.java
 
b/runtime/src/test/java/org/apache/flink/agents/runtime/env/TypedOutputTerminalTest.java
index e99c808a5..4ae759f9c 100644
--- 
a/runtime/src/test/java/org/apache/flink/agents/runtime/env/TypedOutputTerminalTest.java
+++ 
b/runtime/src/test/java/org/apache/flink/agents/runtime/env/TypedOutputTerminalTest.java
@@ -74,7 +74,9 @@ class TypedOutputTerminalTest {
         AgentsExecutionEnvironment agentsEnv =
                 AgentsExecutionEnvironment.getExecutionEnvironment(env);
         DataStream<String> inputStream = env.fromData(List.of("a", "b", "c"));
-        return agentsEnv.fromDataStream(inputStream).apply(new EchoAgent());
+        // The key is irrelevant to what these tests exercise; an explicit 
selector keeps the
+        // builder valid now that an unkeyed input without a selector is 
rejected.
+        return agentsEnv.fromDataStream(inputStream, value -> value).apply(new 
EchoAgent());
     }
 
     @Test

Reply via email to