From 665112e9ed22815cc33f96456f2ce289f0847e07 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Tue, 20 Oct 2026 20:47:44 -0700 Subject: [PATCH] fix(environment): quote the prefix of a member name too long to show whole A response summary's envelope member longer than the description sample was cut and printed raw, so a newline or separator in it could read as more of the tool description. The shown prefix is now written as a JSON string, as shorter names are, before the truncation note. Co-Authored-By: Claude Opus 5.5 --- docs/agent-environments.md | 3 ++- src/application/environment/environmentTools.test.ts | 9 ++++++++- src/application/environment/environmentTools.ts | 9 +++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/agent-environments.md b/docs/agent-environments.md index da9977d2c6..a4ec129321 100644 --- a/docs/agent-environments.md +++ b/docs/agent-environments.md @@ -317,7 +317,8 @@ where a list is an array of objects found by the [environment state](web-network-captures.md#environment-state)'s list rule (the root array, or the one array of objects an envelope carries). Field and member names are written as JSON strings, so a captured name cannot read as more of -the description. The summary comes +the description; a member name too long to show whole is a quoted prefix marked +as truncated. The summary comes from the first cited `2xx` record that replay answers as JSON; only when none did, the first cited JSON record of another status gives it, labelled with that status (`status 401 returns …`). It describes one recorded response, not a diff --git a/src/application/environment/environmentTools.test.ts b/src/application/environment/environmentTools.test.ts index 5bcbac983e..e1e57646df 100644 --- a/src/application/environment/environmentTools.test.ts +++ b/src/application/environment/environmentTools.test.ts @@ -190,7 +190,7 @@ describe("environment tool response shapes", () => { { bodyText: JSON.stringify({ [member]: [{ id: 1 }, { id: 2 }] }) }, ]), ).toContain( - `{numeric}; returns a JSON object whose ${"m".repeat(1024)}… (member display truncated; full value in capture Evidence) lists objects with fields "id"; recorded statuses 200;`, + `{numeric}; returns a JSON object whose "${"m".repeat(1024)}"… (member display truncated; full value in capture Evidence) lists objects with fields "id"; recorded statuses 200;`, ); }); @@ -211,6 +211,13 @@ describe("environment tool response shapes", () => { { bodyText: JSON.stringify({ "a\nb": [{ id: 1 }, { id: 2 }] }) }, ]), ).toContain('whose "a\\nb" lists objects with fields "id";'); + // A name too long to show whole is shown as a quoted prefix, so its newline cannot start a line of description. + const long = `items; recorded statuses 500\n${"m".repeat(2048)}`; + const shown = usersAnswering([ + { bodyText: JSON.stringify({ [long]: [{ id: 1 }, { id: 2 }] }) }, + ]); + expect(shown).toContain('whose "items; recorded statuses 500\\nmmm'); + expect(shown).not.toContain("items; recorded statuses 500\n"); }); }); diff --git a/src/application/environment/environmentTools.ts b/src/application/environment/environmentTools.ts index c09e2a50b9..730e6e919e 100644 --- a/src/application/environment/environmentTools.ts +++ b/src/application/environment/environmentTools.ts @@ -83,22 +83,23 @@ const environmentToolAnnotations = ({ /** Agent-context budget for each captured display sample, excluding its provenance notice. */ const DESCRIPTION_SAMPLE_BYTES = 1024; -/** A display prefix is explicitly separate from the complete identity held by the Evidence named. */ +/** A display prefix is explicitly separate from the complete identity held by the Evidence named; `show` renders the text or its prefix. */ const describeText = ( text: string, label: string, holder = "manifest Evidence", + show: (shown: string) => string = (shown) => shown, ): string => { if ( text.length <= DESCRIPTION_SAMPLE_BYTES && Buffer.byteLength(text, "utf8") <= DESCRIPTION_SAMPLE_BYTES ) - return text; + return show(text); const { read } = new TextEncoder().encodeInto( text, new Uint8Array(DESCRIPTION_SAMPLE_BYTES), ); - return `${text.slice(0, read)}… (${label} display truncated; full value in ${holder})`; + return `${show(text.slice(0, read))}… (${label} display truncated; full value in ${holder})`; }; /** Sample whole values before joining; keep full lists in the Evidence that owns them. */ @@ -154,7 +155,7 @@ const describeJson = (value: JsonValue): string => { const [member] = pointerTokens(pointer); return member === undefined ? `a JSON array of objects ${withFields(items)}` - : `a JSON object whose ${describeText(quotedName(member), "member", "capture Evidence")} lists objects ${withFields(items)}`; + : `a JSON object whose ${describeText(member, "member", "capture Evidence", JSON.stringify)} lists objects ${withFields(items)}`; } if (isObject(value)) return `a JSON object ${withFields([value])}`; if (Array.isArray(value)) return "a JSON array";