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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5075-62883b886837fc1d949e8c29b26b6e8153458d34
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 07be263ab901daad87e8e382c573234364107d6d
Author: Matthew B. <[email protected]>
AuthorDate: Fri May 22 00:01:05 2026 -0700

    fix: filter __is_visualization__ from all result header sites (#5075)
    
    ### What changes were proposed in this PR?
    Operator result rows can carry two internal keys, `__row_index__` and
    `__is_visualization__`. Five sites in agent-service stripped these keys
    before computing column counts or rendering tables, but only one
    stripped both. The other four only stripped `__row_index__`, so
    `__is_visualization__` leaked into the rendered table body, making the
    body one column wider than the reported shape. This PR introduces a
    shared `INTERNAL_RESULT_KEYS` set and `getVisibleResultHeaders` helper
    in `tools utility.ts` and routes all five sites through it, eliminating
    the drift at the source.
      ### Any related issues, documentation, or discussions?
      Closes: #4724
      ### How was this PR tested?
    Added a regression test in `result-formatting.test.ts` covering a row of
    `{ __is_visualization__: false, value: 1 }`, asserting the rendered body
    excludes the `__is_visualization__` column and matches the reported `(1,
    1)` shape. Existing tests for the outer column count and visualization
    payload stripping continue to pass.
      ### Was this PR authored or co-authored using generative AI tooling?
      Co-authored with Claude Opus 4.7 in compliance with ASF
    
    ---------
    
    Signed-off-by: Matthew B. <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../src/agent/tools/result-formatting.test.ts      | 17 +++++++++++
 agent-service/src/agent/tools/result-formatting.ts |  9 ++----
 .../src/agent/tools/tools-utility.test.ts          | 35 ++++++++++++++++++++++
 agent-service/src/agent/tools/tools-utility.ts     |  6 ++++
 .../src/agent/tools/workflow-execution-tools.ts    |  7 +++--
 agent-service/src/server.ts                        |  6 ++--
 6 files changed, 67 insertions(+), 13 deletions(-)

diff --git a/agent-service/src/agent/tools/result-formatting.test.ts 
b/agent-service/src/agent/tools/result-formatting.test.ts
index 19a464c5e2..e6d1afdf2e 100644
--- a/agent-service/src/agent/tools/result-formatting.test.ts
+++ b/agent-service/src/agent/tools/result-formatting.test.ts
@@ -242,6 +242,23 @@ describe("formatOperatorResult - visualization rows", () 
=> {
     expect(out).toContain("<keep/>");
     expect(out).not.toContain("<skipped: visualization content>");
   });
+
+  test("__is_visualization__ column is excluded from rendered table body and 
shape agrees", () => {
+    const out = formatOperatorResult(
+      "op1",
+      makeOpInfo({
+        outputTuples: 1,
+        result: [{ __is_visualization__: false, value: 1 }],
+      }),
+      EMPTY_STATE
+    );
+    const lines = out.split("\n");
+    expect(out).toContain("Output table shape: (1, 1)");
+    // Header line is the third line (after brief summary and shape line).
+    expect(lines[2]).toBe("\tvalue");
+    expect(lines[3]).toBe("0\t1");
+    expect(out).not.toContain("__is_visualization__");
+  });
 });
 
 describe("jsonToTableFormat - cell coercion via formatOperatorResult", () => {
diff --git a/agent-service/src/agent/tools/result-formatting.ts 
b/agent-service/src/agent/tools/result-formatting.ts
index 9a11ba5085..5ed4aacc5d 100644
--- a/agent-service/src/agent/tools/result-formatting.ts
+++ b/agent-service/src/agent/tools/result-formatting.ts
@@ -19,7 +19,7 @@
 
 import type { OperatorInfo } from "../../types/execution";
 import type { WorkflowState } from "../workflow-state";
-import { formatExecuteOperatorResult } from "./tools-utility";
+import { formatExecuteOperatorResult, getVisibleResultHeaders } from 
"./tools-utility";
 
 export function formatOperatorResult(operatorId: string, opInfo: OperatorInfo, 
workflowState: WorkflowState): string {
   if (opInfo.error) {
@@ -31,10 +31,7 @@ export function formatOperatorResult(operatorId: string, 
opInfo: OperatorInfo, w
   }
 
   const jsonArray = opInfo.result as Record<string, any>[];
-  const headers =
-    jsonArray.length > 0
-      ? Object.keys(jsonArray[0]).filter(k => k !== "__row_index__" && k !== 
"__is_visualization__")
-      : [];
+  const headers = jsonArray.length > 0 ? getVisibleResultHeaders(jsonArray[0]) 
: [];
   const columns = headers.length;
 
   const isViz = jsonArray.length > 0 && jsonArray[0]["__is_visualization__"] 
=== true;
@@ -103,7 +100,7 @@ function jsonToTableFormat(jsonResult: Record<string, 
any>[]): string {
   if (!jsonResult || jsonResult.length === 0) return "";
 
   const hasRowIndex = "__row_index__" in jsonResult[0];
-  const headers = Object.keys(jsonResult[0]).filter(h => h !== 
"__row_index__");
+  const headers = getVisibleResultHeaders(jsonResult[0]);
   if (headers.length === 0) return "";
 
   const headerLine = "\t" + headers.join("\t");
diff --git a/agent-service/src/agent/tools/tools-utility.test.ts 
b/agent-service/src/agent/tools/tools-utility.test.ts
index 94043f62f9..b505199709 100644
--- a/agent-service/src/agent/tools/tools-utility.test.ts
+++ b/agent-service/src/agent/tools/tools-utility.test.ts
@@ -25,8 +25,43 @@ import {
   formatModifyOperatorResult,
   formatExecuteOperatorResult,
   formatOperatorError,
+  getVisibleResultHeaders,
 } from "./tools-utility";
 
+describe("getVisibleResultHeaders", () => {
+  test("returns every key when no internal columns are present", () => {
+    expect(getVisibleResultHeaders({ a: 1, b: 2 })).toEqual(["a", "b"]);
+  });
+
+  test("strips __row_index__ from the result", () => {
+    expect(getVisibleResultHeaders({ __row_index__: 0, a: 1 })).toEqual(["a"]);
+  });
+
+  test("strips __is_visualization__ from the result", () => {
+    expect(getVisibleResultHeaders({ __is_visualization__: true, a: 1 
})).toEqual(["a"]);
+  });
+
+  test("strips every known internal column at once", () => {
+    expect(getVisibleResultHeaders({ __row_index__: 0, __is_visualization__: 
true, a: 1, b: 2 })).toEqual(["a", "b"]);
+  });
+
+  test("preserves visible column order", () => {
+    expect(getVisibleResultHeaders({ z: 1, __row_index__: 0, a: 2, 
__is_visualization__: true, m: 3 })).toEqual([
+      "z",
+      "a",
+      "m",
+    ]);
+  });
+
+  test("returns an empty array for an empty row", () => {
+    expect(getVisibleResultHeaders({})).toEqual([]);
+  });
+
+  test("returns an empty array when only internal columns are present", () => {
+    expect(getVisibleResultHeaders({ __row_index__: 0, __is_visualization__: 
true })).toEqual([]);
+  });
+});
+
 describe("createToolResult", () => {
   test("returns the message unchanged", () => {
     expect(createToolResult("ok")).toBe("ok");
diff --git a/agent-service/src/agent/tools/tools-utility.ts 
b/agent-service/src/agent/tools/tools-utility.ts
index 3bc1a1d431..6c9ab004f6 100644
--- a/agent-service/src/agent/tools/tools-utility.ts
+++ b/agent-service/src/agent/tools/tools-utility.ts
@@ -17,6 +17,12 @@
  * under the License.
  */
 
+export const INTERNAL_RESULT_KEYS: ReadonlySet<string> = new 
Set(["__row_index__", "__is_visualization__"]);
+
+export function getVisibleResultHeaders(row: Record<string, any>): string[] {
+  return Object.keys(row).filter(k => !INTERNAL_RESULT_KEYS.has(k));
+}
+
 export function createToolResult(message: string): string {
   return message;
 }
diff --git a/agent-service/src/agent/tools/workflow-execution-tools.ts 
b/agent-service/src/agent/tools/workflow-execution-tools.ts
index 15fa81ff97..78c6cfa3d5 100644
--- a/agent-service/src/agent/tools/workflow-execution-tools.ts
+++ b/agent-service/src/agent/tools/workflow-execution-tools.ts
@@ -19,7 +19,7 @@
 
 import { z } from "zod";
 import { tool } from "ai";
-import { createErrorResult, formatExecuteOperatorResult } from 
"./tools-utility";
+import { createErrorResult, formatExecuteOperatorResult, 
getVisibleResultHeaders } from "./tools-utility";
 import type { WorkflowState } from "../workflow-state";
 import { getBackendConfig } from "../../api/backend-api";
 import { env } from "../../config/env";
@@ -397,7 +397,8 @@ function jsonToTableFormat(jsonResult: Record<string, 
any>[]): string {
   if (!jsonResult || jsonResult.length === 0) return "";
 
   const hasRowIndex = jsonResult.length > 0 && "__row_index__" in 
jsonResult[0];
-  const headers = Object.keys(jsonResult[0]).filter(h => h !== 
"__row_index__");
+  const headers = getVisibleResultHeaders(jsonResult[0]);
+  if (headers.length === 0) return "";
   // Leading tab aligns headers with the index column (pandas __repr__ style).
   const headerLine = "\t" + headers.join("\t");
 
@@ -519,7 +520,7 @@ export async function executeOperatorAndFormat(
     }
 
     const jsonArray = opInfo.result as Record<string, any>[];
-    const headers = jsonArray.length > 0 ? Object.keys(jsonArray[0]).filter(k 
=> k !== "__row_index__") : [];
+    const headers = jsonArray.length > 0 ? 
getVisibleResultHeaders(jsonArray[0]) : [];
     const columns = headers.length;
 
     // Notify for every operator in the execution so upstream stats are also 
stored.
diff --git a/agent-service/src/server.ts b/agent-service/src/server.ts
index a31f9ede11..d5eeae82c9 100644
--- a/agent-service/src/server.ts
+++ b/agent-service/src/server.ts
@@ -21,6 +21,7 @@ import { Elysia, t } from "elysia";
 import { cors } from "@elysiajs/cors";
 import { createOpenAI } from "@ai-sdk/openai";
 import { TexeraAgent } from "./agent/texera-agent";
+import { getVisibleResultHeaders } from "./agent/tools/tools-utility";
 import { getBackendConfig } from "./api/backend-api";
 import { extractUserFromToken, validateToken } from "./api/auth-api";
 import { retrieveWorkflow } from "./api/workflow-api";
@@ -444,10 +445,7 @@ function getOperatorResultSummaries(agent: TexeraAgent): 
Record<string, Operator
       inputTuples: info.inputTuples,
       outputTuples: info.outputTuples,
       inputPortShapes: info.inputPortShapes,
-      outputColumns:
-        info.result && info.result.length > 0
-          ? Object.keys(info.result[0]).filter(k => k !== 
"__row_index__").length
-          : undefined,
+      outputColumns: info.result && info.result.length > 0 ? 
getVisibleResultHeaders(info.result[0]).length : undefined,
       error: info.error,
       warnings: info.warnings,
       consoleLogCount: info.consoleLogs?.length,

Reply via email to