- {commands.map((command, index) =>
-
- {commands.length > 1 ? {zh ? "行" : "Line"} {citation.first_line + index} : null}
- {zh ? "退出码" : "Exit code"} {command.exitCode}{command.status}
-
- {command.output ?
{command.output}
- :
{zh ? "输出为空" : "Empty output"}
}
-
{zh ? "命令参数" : "Command arguments"}
{JSON.stringify(command.argv, null, 2)}
+ {records.map((record, index) =>
+ {"texts" in record ? <>
+ {records.length > 1 ?
{zh ? "行" : "Line"} {citation.first_line + index}
: null}
+ {record.texts.map((text, part) =>
)}
+ > : <>
+
+ {records.length > 1 ? {zh ? "行" : "Line"} {citation.first_line + index} : null}
+ {zh ? "退出码" : "Exit code"} {record.exitCode}{record.status}
+
+
+ {zh ? "命令参数" : "Command arguments"}
{JSON.stringify(record.argv, null, 2)}
+ >}
)}
{zh ? "完整原始记录" : "Complete raw record"}
{citation.text}
;
diff --git a/docs/reference/trajectory-assessment.md b/docs/reference/trajectory-assessment.md
index 01616f0555..d99cc9ac6f 100644
--- a/docs/reference/trajectory-assessment.md
+++ b/docs/reference/trajectory-assessment.md
@@ -27,6 +27,14 @@ the history action returns to the complete loaded research history without
changing the selected evidence pack. Reading either layer, switching report
tabs or opening an original never executes an analysis or experiment.
+The source reader displays saved completed-command output and text-only
+`function_call_output` / `custom_tool_call_output` records in source order.
+Direct shell-result envelopes show their literal output and observed exit code;
+a missing exit code remains unknown. Command arguments and the complete original
+record stay available on demand. Mixed, malformed or unsupported content falls
+back to the complete literal citation. Unknown nested transport stays text;
+reading never recursively interprets it or turns an exit code into acceptance.
+
The presentation draws on composable inspection in
[Distill's interpretability article](https://distill.pub/2018/building-blocks/)
and responsive comparison layouts in the
diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-after-desktop.png b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-after-desktop.png
new file mode 100644
index 0000000000..8beed2ccca
Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-after-desktop.png differ
diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-after-phone.png b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-after-phone.png
new file mode 100644
index 0000000000..1d7f6ff5ed
Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-after-phone.png differ
diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-before-desktop.png b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-before-desktop.png
new file mode 100644
index 0000000000..ec9e7352cf
Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-before-desktop.png differ
diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-before-phone.png b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-before-phone.png
new file mode 100644
index 0000000000..8ba0c0a77e
Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/tool-output-before-phone.png differ
diff --git a/examples/personal-workspace-browser/research-native-reading.mjs b/examples/personal-workspace-browser/research-native-reading.mjs
index db8d99fd30..cbac956f50 100644
--- a/examples/personal-workspace-browser/research-native-reading.mjs
+++ b/examples/personal-workspace-browser/research-native-reading.mjs
@@ -25,7 +25,8 @@ export const researchNativeReadingScenario = {
expected: [0.0000001234567890123456, 0.0000001234567890123456, 0.0000001123456789012345, 0.0000001123456789012345], display: ["1.235E-7", "1.235E-7", "1.123E-7", "1.123E-7"], gain: "-1.111E-8"},
{id: "large", values: [123456789.12345679], higher: true, retained: 0,
expected: [123456789.12345679, 123456789.12345679, 123456789.12345679, 123456789.12345679], display: ["1.235E8", "1.235E8", "1.235E8", "1.235E8"], gain: "0"},
- ...["command", "mixed", "damaged", "empty-output", "unsafe-exit"].map(source => ({id: `${source}-source`, source,
+ ...["command", "mixed", "damaged", "empty-output", "unsafe-exit", "tool-text", "tool-shell", "tool-pending",
+ "tool-function", "tool-mixed", "tool-damaged", "tool-unsafe-exit", "tool-empty"].map(source => ({id: `${source}-source`, source,
values: [2, 1], higher: false, retained: 0, expected: [1, 2, 1, 2]})),
];
const root = await mkdtemp(resolve(tmpdir(), "loopx-native-reading-"));
@@ -37,6 +38,9 @@ export const researchNativeReadingScenario = {
fixture.analyses = cases.map(c => {
const a = structuredClone(original), native = JSON.parse(a.artifacts["native.json"]);
a.title = c.id;
+ // These supplied outputs use the legacy assessment format, independent
+ // of the current registration default for newly generated reports.
+ a.config = {model: "fixture", effort: "max", service_tier: "fast", research_synthesis: false};
a.input.scope.run_id = c.id;
native.scope = {...a.input.scope};
native.complete = !c.partial;
@@ -62,6 +66,20 @@ export const researchNativeReadingScenario = {
? JSON.stringify({type: "unknown_event", text: "Unrecognized source MUST_REMAIN_LITERAL"})
: c.source === "damaged" ? "{malformed MUST_REMAIN_LITERAL"
: command(c.source === "empty-output" ? "" : longFailure, c.source === "unsafe-exit" ? Number.MAX_SAFE_INTEGER + 1 : 17)].join("\n");
+ if (c.source.startsWith("tool-")) {
+ const shell = (text, exit_code) => JSON.stringify({chunk_id: "synthetic", wall_time_seconds: 0.25, exit_code, output: text});
+ const tool = output => JSON.stringify({type: "response_item", payload: {
+ type: c.source === "tool-function" ? "function_call_output" : "custom_tool_call_output", output}});
+ const part = text => ({type: "input_text", text});
+ const first = ["tool-text", "tool-function"].includes(c.source) ? output : c.source === "tool-empty" ? ""
+ : [part("Recorded tool response\n"), part(c.source === "tool-unsafe-exit"
+ ? '{"chunk_id":"synthetic","wall_time_seconds":0.25,"exit_code":9007199254740993,"output":"MUST_REMAIN_LITERAL"}'
+ : shell(output, c.source === "tool-pending" ? null : 0))];
+ const second = c.source === "tool-mixed" ? [part(longFailure), {type: "input_image", image_url: "synthetic"}]
+ : c.source === "tool-damaged" ? [part(longFailure), {type: "input_text", text: 12}]
+ : c.source === "tool-empty" ? "" : [part(shell(longFailure, 17))];
+ text = [tool(first), tool(second)].join("\n");
+ }
native.samples[0].citation.last_line = 2;
}
a.artifacts["facts.txt"] = text;
@@ -73,7 +91,7 @@ export const researchNativeReadingScenario = {
await writeFile(path, JSON.stringify(fixture));
// Same public synthetic data for reproducible before/after inspection.
await writeFile(resolve(outputDir, "research-citation-reader-fixture.json"), JSON.stringify({...fixture,
- analyses: fixture.analyses.filter(a => a.title === "command-source")}));
+ analyses: fixture.analyses.filter(a => ["command-source", "tool-shell-source"].includes(a.title))}));
owner = await startHistoryOwner(path);
page = await browser.newPage({viewport: {width: 1280, height: 900}, hasTouch: true});
const errors = [];
@@ -139,7 +157,7 @@ export const researchNativeReadingScenario = {
const citation = page.getByRole("region", {name: "引用原文", exact: true});
await citation.waitFor();
const literal = fixture.analyses.find(a => a.title === c.id).artifacts["facts.txt"];
- if (["mixed", "damaged", "unsafe-exit"].includes(c.source)) {
+ if (["mixed", "damaged", "unsafe-exit", "tool-mixed", "tool-damaged"].includes(c.source)) {
await citation.locator("pre").waitFor();
assert.equal(await citation.locator("pre").textContent(), literal, "Never discard unsupported or malformed neighboring evidence");
assert.equal(await citation.locator("details").count(), 0);
@@ -147,17 +165,28 @@ export const researchNativeReadingScenario = {
await citation.locator(".analysis-source-content").waitFor();
assert.equal(await citation.locator(".analysis-source-command").count(), 2);
await citation.getByText("行 2", {exact: true}).waitFor();
- await citation.getByText("退出码 17", {exact: true}).waitFor();
+ if (c.source !== "tool-empty") await citation.getByText("退出码 17", {exact: true}).waitFor();
+ if (c.source === "tool-pending") await citation.getByText("尚无退出码", {exact: true}).waitFor();
const raw = citation.locator(".analysis-source-raw");
assert.equal(await raw.getAttribute("open"), null);
await raw.locator("summary").tap();
assert.equal(await raw.locator("pre").textContent(), literal, "Raw evidence remains byte-for-byte available");
await raw.locator("summary").tap();
- if (c.source === "empty-output") assert.equal(await citation.getByText("输出为空", {exact: true}).count(), 2);
+ if (["empty-output", "tool-empty"].includes(c.source)) assert.equal(await citation.getByText("输出为空", {exact: true}).count(), 2);
else {
- const records = literal.split("\n").map(line => JSON.parse(line).payload.item);
- for (const [i, record] of records.entries()) {
- assert.equal(await citation.locator(".analysis-source-output").nth(i).textContent(), record.aggregated_output);
+ const records = literal.split("\n").map(line => JSON.parse(line).payload);
+ const outputs = await citation.locator(".analysis-source-output").allTextContents();
+ if (c.source.startsWith("tool-")) {
+ const main = JSON.stringify({cases: 8, improved: 3, regressed: 2, unchanged: 3, complete_cost: null}, null, 2);
+ assert(outputs.includes(c.source === "tool-unsafe-exit" ? records[0].output[1].text : main));
+ if (c.source === "tool-unsafe-exit") {
+ assert.equal(await citation.getByText("退出码 9007199254740992", {exact: true}).count(), 0, "Unsafe numeric metadata must not be rounded into a display fact");
+ assert(outputs.some(text => text.includes("9007199254740993")));
+ }
+ if (["tool-shell", "tool-pending", "tool-unsafe-exit"].includes(c.source)) assert.equal(outputs[0], "Recorded tool response\n", "Preserve text blocks and order, including surrounding caveats");
+ assert.equal(outputs.at(-1), JSON.parse(records[1].output[0].text).output);
+ } else {
+ for (const [i, record] of records.entries()) assert.equal(outputs[i], record.item.aggregated_output);
}
assert.equal(await citation.locator("script").count(), 0);
assert.equal(await page.evaluate(() => window.sourceExecuted), undefined);
@@ -191,12 +220,13 @@ export const researchNativeReadingScenario = {
await page.getByRole("button", {name: "About native measurements", exact: true}).focus();
await page.getByRole("tooltip").waitFor({state: "visible"});
await page.keyboard.press("Escape");
- await selector.selectOption({label: "command-source"});
+ await selector.selectOption({label: "tool-pending-source"});
const selected = await selector.inputValue();
await page.locator(`.research-progress-scope[data-pack-id="${selected}"]`).waitFor();
await page.getByRole("region", {name: "Native measurements", exact: true}).getByRole("button", {name: /Native retained result.*Read original evidence/}).click();
const originalPanel = page.getByRole("region", {name: "Cited original", exact: true});
await originalPanel.getByText("Exit code 17", {exact: true}).waitFor();
+ await originalPanel.getByText("No exit code yet", {exact: true}).waitFor();
await originalPanel.getByText("Complete raw record", {exact: true}).waitFor();
await originalPanel.press("Escape");
const readback = await (await page.request.get(`${owner.base}/fixture/readback`)).json();