fix(agents): land #38935 from @MumuTW

Co-authored-by: MumuTW <MumuTW@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-03-07 18:55:49 +00:00
parent 231c1fa37a
commit 5effa6043e
3 changed files with 54 additions and 12 deletions

View File

@@ -144,4 +144,35 @@ describe("createCacheTrace", () => {
expect(source.bytes).toBe(6);
expect(source.sha256).toBe(crypto.createHash("sha256").update("U0VDUkVU").digest("hex"));
});
it("handles circular references in messages without stack overflow", () => {
const lines: string[] = [];
const trace = createCacheTrace({
cfg: {
diagnostics: {
cacheTrace: {
enabled: true,
},
},
},
env: {},
writer: {
filePath: "memory",
write: (line) => lines.push(line),
},
});
const parent: Record<string, unknown> = { role: "user", content: "hello" };
const child: Record<string, unknown> = { ref: parent };
parent.child = child; // circular reference
trace?.recordStage("prompt:images", {
messages: [parent] as unknown as [],
});
expect(lines.length).toBe(1);
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
expect(event.messageCount).toBe(1);
expect(event.messageFingerprints).toHaveLength(1);
});
});

View File

@@ -104,7 +104,7 @@ function getWriter(filePath: string): CacheTraceWriter {
return getQueuedFileWriter(writers, filePath);
}
function stableStringify(value: unknown): string {
function stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {
if (value === null || value === undefined) {
return String(value);
}
@@ -117,30 +117,40 @@ function stableStringify(value: unknown): string {
if (typeof value !== "object") {
return JSON.stringify(value) ?? "null";
}
if (seen.has(value)) {
return JSON.stringify("[Circular]");
}
seen.add(value);
if (value instanceof Error) {
return stableStringify({
name: value.name,
message: value.message,
stack: value.stack,
});
return stableStringify(
{
name: value.name,
message: value.message,
stack: value.stack,
},
seen,
);
}
if (value instanceof Uint8Array) {
return stableStringify({
type: "Uint8Array",
data: Buffer.from(value).toString("base64"),
});
return stableStringify(
{
type: "Uint8Array",
data: Buffer.from(value).toString("base64"),
},
seen,
);
}
if (Array.isArray(value)) {
const serializedEntries: string[] = [];
for (const entry of value) {
serializedEntries.push(stableStringify(entry));
serializedEntries.push(stableStringify(entry, seen));
}
return `[${serializedEntries.join(",")}]`;
}
const record = value as Record<string, unknown>;
const serializedFields: string[] = [];
for (const key of Object.keys(record).toSorted()) {
serializedFields.push(`${JSON.stringify(key)}:${stableStringify(record[key])}`);
serializedFields.push(`${JSON.stringify(key)}:${stableStringify(record[key], seen)}`);
}
return `{${serializedFields.join(",")}}`;
}