This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 07be263ab9 fix: filter __is_visualization__ from all result header
sites (#5075)
07be263ab9 is described below
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,