mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
perf(cli): write compact JSON when output is not a terminal (#1413)
`--json` / `--format json` always indented its streamed result. Agents and scripts read REA through pipes and redirects, where indentation carries no information: on real application results it added 16-45% model tokens (o200k proxy) and 25-73% bytes. The skill's CLI fallback uses `--json`. Indent only when stdout is a TTY, as `gh --json` does; piped or redirected JSON is compact. The JSON value, `jsonl`, `--filter-output`, `--full-output` and the token-window paths are unchanged. `jq .` restores indentation for files. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
de8fc67195
commit
ca3131b36c
+4
-1
@@ -179,7 +179,10 @@ user permissions; consult the chosen guide for host requirements and effects.
|
||||
## Output and exit status
|
||||
|
||||
The default terminal format is TOON. Use `--json` when saving results for a
|
||||
JSON consumer. Output selection and formatting do not change operation status.
|
||||
JSON consumer. `--json` is indented on a terminal and compact when piped or
|
||||
redirected, which keeps large results smaller for agents and scripts; pipe
|
||||
through `jq .` for indented files. Output selection and formatting do not change
|
||||
operation status.
|
||||
|
||||
| Status | Meaning |
|
||||
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -54,15 +54,15 @@ try {
|
||||
data: { ...evidence, normalized_result: { payload: [marker] } },
|
||||
meta: metadata,
|
||||
};
|
||||
const skeleton = JSON.stringify(template, null, format === "json" ? 2 : 0);
|
||||
// The destination is a redirected file, so both formats are compact.
|
||||
const skeleton = JSON.stringify(template);
|
||||
const encodedMarker = JSON.stringify(marker);
|
||||
const markerIndex = skeleton.indexOf(encodedMarker);
|
||||
assert.ok(markerIndex >= 0);
|
||||
assert.equal(markerIndex, skeleton.lastIndexOf(encodedMarker));
|
||||
const prefix = skeleton.slice(0, markerIndex);
|
||||
const suffix = `${skeleton.slice(markerIndex + encodedMarker.length)}\n`;
|
||||
const indentation = prefix.slice(prefix.lastIndexOf("\n") + 1);
|
||||
const separator = format === "json" ? `,\n${indentation}` : ",";
|
||||
const separator = ",";
|
||||
const expectedHash = createHash("sha256");
|
||||
expectedHash.update(prefix);
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
|
||||
+15
-3
@@ -4,16 +4,27 @@ import { bufferedJsonParts, jsonParts } from "../domain/jsonSerialization.js";
|
||||
|
||||
type JsonOutputFormat = "json" | "jsonl";
|
||||
|
||||
/** Write ordinary JSON without constructing a document-sized string. */
|
||||
/**
|
||||
* Write ordinary JSON without constructing a document-sized string.
|
||||
*
|
||||
* `json` is indented for a terminal reader. Piped or redirected output, which
|
||||
* agents and scripts consume, is compact: indentation adds no information and
|
||||
* measured 16-45% more model tokens on real results. The value is identical.
|
||||
*/
|
||||
export const writeJsonOutput = async (
|
||||
value: unknown,
|
||||
destination: Writable,
|
||||
format: JsonOutputFormat = "json",
|
||||
): Promise<void> => {
|
||||
for (const part of bufferedJsonParts(jsonOutputParts(value, format)))
|
||||
for (const part of bufferedJsonParts(
|
||||
jsonOutputParts(value, format, isTerminal(destination)),
|
||||
))
|
||||
await writePart(destination, part);
|
||||
};
|
||||
|
||||
const isTerminal = (destination: Writable): boolean =>
|
||||
"isTTY" in destination && destination.isTTY === true;
|
||||
|
||||
const writePart = (destination: Writable, part: string): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const onError = (cause: unknown): void => reject(cause);
|
||||
@@ -39,6 +50,7 @@ const writePart = (destination: Writable, part: string): Promise<void> =>
|
||||
export function* jsonOutputParts(
|
||||
value: unknown,
|
||||
format: JsonOutputFormat,
|
||||
pretty = true,
|
||||
): Generator<string> {
|
||||
if (format === "jsonl" && Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
@@ -47,6 +59,6 @@ export function* jsonOutputParts(
|
||||
}
|
||||
return;
|
||||
}
|
||||
yield* jsonParts(value, format === "json");
|
||||
yield* jsonParts(value, pretty && format === "json");
|
||||
yield "\n";
|
||||
}
|
||||
|
||||
@@ -83,18 +83,22 @@ it("reports real HAR heap exhaustion through CLI and MCP and preserves server us
|
||||
});
|
||||
expect(JSON.stringify(response)).not.toContain("caller-marked");
|
||||
expect(JSON.stringify(response)).not.toContain("rea doctor");
|
||||
await expect(
|
||||
execute(process.execPath, [
|
||||
cli,
|
||||
"inspect-web-network-capture",
|
||||
path,
|
||||
"har",
|
||||
"--json",
|
||||
]),
|
||||
).rejects.toMatchObject({
|
||||
code: 1,
|
||||
stdout: expect.stringContaining('"code": "resource_constraint"'),
|
||||
});
|
||||
const failed: unknown = await execute(process.execPath, [
|
||||
cli,
|
||||
"inspect-web-network-capture",
|
||||
path,
|
||||
"har",
|
||||
"--json",
|
||||
]).then(
|
||||
() => undefined,
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
expect(failed).toMatchObject({ code: 1 });
|
||||
const stdout =
|
||||
typeof failed === "object" && failed !== null && "stdout" in failed
|
||||
? String(failed.stdout)
|
||||
: "";
|
||||
expect(JSON.parse(stdout)).toMatchObject({ code: "resource_constraint" });
|
||||
const small = join(root, "small.har");
|
||||
await writeFile(small, capture(2));
|
||||
await client.ping();
|
||||
|
||||
@@ -16,6 +16,7 @@ import { silentLogger } from "../../../src/logger.js";
|
||||
const collect = async (
|
||||
value: unknown,
|
||||
format: "json" | "jsonl" = "json",
|
||||
terminal = true,
|
||||
): Promise<string> => {
|
||||
const chunks: Buffer[] = [];
|
||||
const destination = new Writable({
|
||||
@@ -28,6 +29,7 @@ const collect = async (
|
||||
callback();
|
||||
},
|
||||
});
|
||||
if (terminal) Object.defineProperty(destination, "isTTY", { value: true });
|
||||
await writeJsonOutput(value, destination, format);
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
};
|
||||
@@ -52,6 +54,16 @@ describe("incremental CLI JSON", () => {
|
||||
expect(await collect(value)).toBe(`${JSON.stringify(value, null, 2)}\n`);
|
||||
});
|
||||
|
||||
it("indents JSON only for a terminal and writes piped JSON compactly", async () => {
|
||||
const value = { nested: [{ value: null }], text: "雪𝟠" };
|
||||
expect(await collect(value, "json", true)).toBe(
|
||||
`${JSON.stringify(value, null, 2)}\n`,
|
||||
);
|
||||
expect(await collect(value, "json", false)).toBe(
|
||||
`${JSON.stringify(value)}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it("writes compact JSONL records and preserves an empty record set", async () => {
|
||||
const value = [{ text: "first\nrecord" }, { text: "雪𝟠" }];
|
||||
expect(await collect(value, "jsonl")).toBe(
|
||||
@@ -78,6 +90,7 @@ describe("incremental CLI JSON", () => {
|
||||
}, 1);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(destination, "isTTY", { value: true });
|
||||
await writeJsonOutput({ leaf }, destination);
|
||||
expect(largestChunk).toBeLessThan(Buffer.byteLength(leaf));
|
||||
const actual = Buffer.concat(chunks).toString("utf8");
|
||||
@@ -212,6 +225,8 @@ describe("streamed CLI result surface", () => {
|
||||
expect(operation).toBe("inspect_managed_members");
|
||||
return value;
|
||||
});
|
||||
// A terminal destination exercises the indented encoding past the limit.
|
||||
Object.defineProperty(destination, "isTTY", { value: true });
|
||||
const argv = ["inspect-managed-members", "fixture.dll", "--json"];
|
||||
const output = createStreamedCliJsonOutput(argv, destination);
|
||||
if (output === undefined) throw new Error("Missing JSON output surface");
|
||||
|
||||
Reference in New Issue
Block a user