Skip to content

Commit a086df1

Browse files
committed
Enhance payload codec support and automatic selection
- Added support for `deflate` codec in payload encoding and decoding processes. - Implemented packed wire mode for more efficient transport of payloads. - Updated codec selection logic to prioritize `deflate`, followed by `lz`, and then `plain`, optimizing fragment size based on available options. - Enhanced documentation to reflect new codec capabilities and usage guidelines. - Added tests for new codec functionality and budget-aware encoding.
1 parent 1c2fb2f commit a086df1

13 files changed

Lines changed: 649 additions & 42 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Treat these as core constraints unless the owner explicitly changes the product
2626
- Artifact payloads live in the URL fragment, using `#agent-render=v1.<codec>.<payload>`.
2727
- The deployed host should not receive artifact contents as part of the initial page request.
2828
- Supported artifact kinds are `markdown`, `code`, `diff`, `csv`, and `json`.
29-
- Supported codecs are `plain` and `lz`.
29+
- Supported codecs are `plain`, `lz`, and `deflate`.
3030
- The product is zero-retention by host design, not secret-safe in an absolute sense.
3131
- Links may still leak through browser history, copied URLs, screenshots, and any future client-side analytics.
3232

@@ -83,9 +83,10 @@ The fragment transport is part of the product surface, not an implementation det
8383
Current rules:
8484
- fragment key: `agent-render`
8585
- format: `v1.<codec>.<payload>`
86-
- codecs: `plain` and `lz`
86+
- codecs: `plain`, `lz`, and `deflate`
8787
- fragment size budget: `8000` characters
8888
- decoded payload budget: `200000` characters
89+
- packed wire transport (`p: 1`) is allowed and must decode back to the standard envelope
8990
- bundles must contain at least one artifact
9091
- artifact ids must be unique within a bundle
9192
- invalid `activeArtifactId` values normalize to the first artifact

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based shari
1515
## Status
1616

1717
- Markdown, code, diff, CSV, and JSON all render in the static shell
18-
- Fragment transport now supports `plain` and compressed `lz` codecs, with compression chosen automatically when it helps
18+
- Fragment transport supports `plain`, `lz`, and `deflate`, with automatic shortest-fragment selection across packed/non-packed wire formats
1919
- Markdown supports download plus browser print-to-PDF
2020
- Deployment target: static hosting, including Cloudflare Pages
2121

@@ -62,7 +62,7 @@ NEXT_PUBLIC_BASE_PATH=/agent-render npm run build
6262

6363
The home page includes sample fragment presets for every artifact type, including a malformed JSON case for error handling.
6464

65-
The fragment examples are encoded with the same transport used by the app, so larger samples will naturally switch to compressed `lz` transport.
65+
The fragment examples are encoded with the same transport used by the app, so larger samples naturally switch to the shortest available transport.
6666

6767
## Bundle Notes
6868

docs/architecture.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ The fragment protocol keeps the JSON envelope stable and treats compression stri
8787

8888
- `plain` stores base64url-encoded JSON for compatibility and debugging
8989
- `lz` stores compressed JSON via `lz-string` when it produces a smaller fragment
90+
- `deflate` stores deflate-compressed UTF-8 JSON bytes when it outperforms other codecs
91+
- packed wire mode (`p: 1`) shortens transport keys before compression, then unpacks back to the standard envelope during decode
92+
- automatic codec selection now tries `deflate -> lz -> plain` and compares packed + non-packed candidates
9093
- decode enforces both fragment length and decoded payload size ceilings before UI rendering
9194
- invalid bundle state is normalized or rejected before renderers mount
9295

docs/dependency-notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- `papaparse` - MIT
1515
- `@tanstack/react-table` - MIT
1616
- `lz-string` - MIT
17+
- `fflate` - MIT
1718

1819
## Notes
1920

@@ -28,6 +29,7 @@
2829
- `@replit/codemirror-indentation-markers` replaces custom indent-guide logic with a maintained CM6 extension.
2930
- `@git-diff-view/*` fits review-style diffs better than a generic merge editor for the current viewer.
3031
- `papaparse` plus `@tanstack/react-table` keeps CSV parsing and rendering readable without coupling to a heavyweight data-grid framework.
32+
- `fflate` provides portable deflate/inflate support across iOS Safari and Android Chromium without relying on browser-specific compression streams.
3133

3234
## Notable removals
3335

docs/payload-format.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Supported codecs:
1616

1717
- `plain` - base64url-encoded JSON
1818
- `lz` - `lz-string` compressed JSON encoded for URL-safe transport
19+
- `deflate` - deflate-compressed UTF-8 JSON bytes encoded as base64url
20+
21+
The encoder now also supports a packed wire representation (`p: 1`) that shortens key names before compression. Packed mode is transport-only; decoded envelopes normalize back to the standard shape.
1922

2023
## Envelope
2124

@@ -37,6 +40,31 @@ Supported codecs:
3740
}
3841
```
3942

43+
Packed wire envelopes are also valid on the wire:
44+
45+
```json
46+
{
47+
"p": 1,
48+
"v": 1,
49+
"c": "deflate",
50+
"t": "Artifact bundle title",
51+
"a": "artifact-1",
52+
"r": [
53+
{
54+
"i": "artifact-1",
55+
"k": "markdown",
56+
"f": "weekly-report.md",
57+
"c": "# Report"
58+
}
59+
]
60+
}
61+
```
62+
63+
Packed key map:
64+
65+
- envelope: `codec -> c`, `title -> t`, `activeArtifactId -> a`, `artifacts -> r`
66+
- artifact: `id -> i`, `kind -> k`, `title -> t`, `filename -> f`, `content -> c`, `language -> l`, `patch -> p`, `oldContent -> o`, `newContent -> n`, `view -> w`
67+
4068
## Required support
4169

4270
- `kind`
@@ -50,7 +78,22 @@ Supported codecs:
5078
- Supported fragment budget: 8,000 characters
5179
- Supported decoded payload budget: 200,000 characters
5280
- Larger payloads should fail with a clear error before rendering
53-
- Compression is enabled now and selected automatically when the `lz` form is shorter than `plain`
81+
- Compression is selected automatically by shortest fragment across packed/non-packed candidates
82+
- Default codec priority is `deflate -> lz -> plain`
83+
- Optional budget-aware encoding can target strict limits like 1,500 chars and returns the shortest fragment when none fit
84+
85+
### AGENTS.md POC benchmark
86+
87+
Running `npm run codec:poc` (single markdown artifact containing `AGENTS.md`) currently yields:
88+
89+
- `plain`: 10,584 chars
90+
- `plain+packed`: 10,522 chars
91+
- `lz`: 5,622 chars
92+
- `lz+packed`: 5,583 chars
93+
- `deflate`: 4,328 chars
94+
- `deflate+packed`: 4,312 chars (best)
95+
96+
Result: aggressive transport improves size materially (~23.3% vs `lz` baseline), but this payload still does not fit a 1,500-char budget.
5497

5598
## Active artifact behavior
5699

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"test:e2e:update": "playwright test --update-snapshots",
1616
"test:ci": "npm run lint && npm run test && npm run typecheck && npm run test:e2e",
1717
"test:browsers": "playwright install chromium",
18+
"codec:poc": "node scripts/codec-poc.mjs",
1819
"typecheck": "node scripts/ensure-next-types.mjs && tsc --noEmit",
1920
"check": "npm run lint && npm run test && npm run typecheck && npm run build"
2021
},
@@ -31,13 +32,14 @@
3132
"@codemirror/search": "^6.5.10",
3233
"@codemirror/state": "^6.5.2",
3334
"@codemirror/view": "^6.38.2",
34-
"@replit/codemirror-indentation-markers": "^6.5.3",
3535
"@git-diff-view/file": "^0.1.1",
3636
"@git-diff-view/react": "^0.1.1",
37+
"@replit/codemirror-indentation-markers": "^6.5.3",
3738
"@tanstack/react-table": "^8.21.3",
3839
"clsx": "^2.1.1",
39-
"lz-string": "^1.5.0",
40+
"fflate": "^0.8.2",
4041
"lucide-react": "^0.577.0",
42+
"lz-string": "^1.5.0",
4143
"next": "15.1.11",
4244
"next-themes": "^0.4.6",
4345
"papaparse": "^5.5.3",

scripts/codec-poc.mjs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { readFile } from "node:fs/promises";
2+
import lzString from "lz-string";
3+
import { deflateSync, inflateSync, strFromU8, strToU8 } from "fflate";
4+
5+
const FRAGMENT_KEY = "agent-render";
6+
const TARGET_BUDGET = 1500;
7+
const { compressToEncodedURIComponent, decompressFromEncodedURIComponent } = lzString;
8+
9+
function toBase64UrlBytes(bytes) {
10+
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
11+
const base64 = btoa(binary);
12+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
13+
}
14+
15+
function fromBase64UrlBytes(input) {
16+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
17+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
18+
const binary = atob(`${normalized}${padding}`);
19+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
20+
}
21+
22+
function encodePayload(json, codec) {
23+
switch (codec) {
24+
case "plain":
25+
return toBase64UrlBytes(strToU8(json));
26+
case "lz":
27+
return compressToEncodedURIComponent(json);
28+
case "deflate":
29+
return toBase64UrlBytes(deflateSync(strToU8(json)));
30+
default:
31+
throw new Error(`Unsupported codec: ${codec}`);
32+
}
33+
}
34+
35+
function decodePayload(encoded, codec) {
36+
switch (codec) {
37+
case "plain":
38+
return strFromU8(fromBase64UrlBytes(encoded));
39+
case "lz":
40+
return decompressFromEncodedURIComponent(encoded);
41+
case "deflate":
42+
return strFromU8(inflateSync(fromBase64UrlBytes(encoded)));
43+
default:
44+
throw new Error(`Unsupported codec: ${codec}`);
45+
}
46+
}
47+
48+
function packEnvelope(envelope) {
49+
return {
50+
p: 1,
51+
v: envelope.v,
52+
c: envelope.codec,
53+
t: envelope.title,
54+
a: envelope.activeArtifactId,
55+
r: envelope.artifacts.map((artifact) => {
56+
if (artifact.kind === "code") {
57+
return {
58+
i: artifact.id,
59+
k: artifact.kind,
60+
t: artifact.title,
61+
f: artifact.filename,
62+
c: artifact.content,
63+
l: artifact.language,
64+
};
65+
}
66+
67+
if (artifact.kind === "diff") {
68+
return {
69+
i: artifact.id,
70+
k: artifact.kind,
71+
t: artifact.title,
72+
f: artifact.filename,
73+
p: artifact.patch,
74+
o: artifact.oldContent,
75+
n: artifact.newContent,
76+
l: artifact.language,
77+
w: artifact.view,
78+
};
79+
}
80+
81+
return {
82+
i: artifact.id,
83+
k: artifact.kind,
84+
t: artifact.title,
85+
f: artifact.filename,
86+
c: artifact.content,
87+
};
88+
}),
89+
};
90+
}
91+
92+
function unpackEnvelope(value) {
93+
if (!value || value.p !== 1 || !Array.isArray(value.r)) {
94+
return value;
95+
}
96+
97+
return {
98+
v: value.v,
99+
codec: value.c,
100+
title: value.t,
101+
activeArtifactId: value.a,
102+
artifacts: value.r.map((artifact) => {
103+
if (artifact.k === "code") {
104+
return {
105+
id: artifact.i,
106+
kind: "code",
107+
title: artifact.t,
108+
filename: artifact.f,
109+
content: artifact.c,
110+
language: artifact.l,
111+
};
112+
}
113+
114+
if (artifact.k === "diff") {
115+
return {
116+
id: artifact.i,
117+
kind: "diff",
118+
title: artifact.t,
119+
filename: artifact.f,
120+
patch: artifact.p,
121+
oldContent: artifact.o,
122+
newContent: artifact.n,
123+
language: artifact.l,
124+
view: artifact.w,
125+
};
126+
}
127+
128+
return {
129+
id: artifact.i,
130+
kind: artifact.k,
131+
title: artifact.t,
132+
filename: artifact.f,
133+
content: artifact.c,
134+
};
135+
}),
136+
};
137+
}
138+
139+
function buildFragment(envelope, codec, packed) {
140+
const json = JSON.stringify(packed ? packEnvelope({ ...envelope, codec }) : { ...envelope, codec });
141+
return `${FRAGMENT_KEY}=v1.${codec}.${encodePayload(json, codec)}`;
142+
}
143+
144+
function parseFragment(fragment) {
145+
const payload = fragment.startsWith("#") ? fragment.slice(1) : fragment;
146+
const [key, value] = payload.split("=", 2);
147+
if (key !== FRAGMENT_KEY || !value) {
148+
throw new Error("Invalid fragment key");
149+
}
150+
151+
const firstDot = value.indexOf(".");
152+
const secondDot = value.indexOf(".", firstDot + 1);
153+
const codec = value.slice(firstDot + 1, secondDot);
154+
const encoded = value.slice(secondDot + 1);
155+
const decodedJson = decodePayload(encoded, codec);
156+
if (decodedJson == null) {
157+
throw new Error("Codec decode returned null");
158+
}
159+
160+
return unpackEnvelope(JSON.parse(decodedJson));
161+
}
162+
163+
function stableJson(value) {
164+
return JSON.stringify(value);
165+
}
166+
167+
async function main() {
168+
const markdown = await readFile(new URL("../AGENTS.md", import.meta.url), "utf8");
169+
const envelope = {
170+
v: 1,
171+
codec: "plain",
172+
title: "AGENTS.md POC",
173+
activeArtifactId: "agents",
174+
artifacts: [
175+
{
176+
id: "agents",
177+
kind: "markdown",
178+
title: "AGENTS.md",
179+
filename: "AGENTS.md",
180+
content: markdown,
181+
},
182+
],
183+
};
184+
185+
const variants = [
186+
{ name: "plain", codec: "plain", packed: false },
187+
{ name: "plain+packed", codec: "plain", packed: true },
188+
{ name: "lz", codec: "lz", packed: false },
189+
{ name: "lz+packed", codec: "lz", packed: true },
190+
{ name: "deflate", codec: "deflate", packed: false },
191+
{ name: "deflate+packed", codec: "deflate", packed: true },
192+
];
193+
194+
const baseline = buildFragment(envelope, "lz", false).length;
195+
const rows = variants.map((variant) => {
196+
const fragment = buildFragment(envelope, variant.codec, variant.packed);
197+
const decoded = parseFragment(fragment);
198+
const ok = stableJson(decoded) === stableJson({ ...envelope, codec: variant.codec });
199+
return {
200+
variant: variant.name,
201+
length: fragment.length,
202+
fits1500: fragment.length <= TARGET_BUDGET,
203+
deltaVsLz: fragment.length - baseline,
204+
pctVsLz: `${(((fragment.length - baseline) / baseline) * 100).toFixed(2)}%`,
205+
roundTrip: ok ? "ok" : "mismatch",
206+
};
207+
});
208+
209+
console.log("AGENTS.md codec POC");
210+
console.log(JSON.stringify({ budget: TARGET_BUDGET, lzBaseline: baseline, rows }, null, 2));
211+
}
212+
213+
await main();

0 commit comments

Comments
 (0)