Skip to content

Commit 85753fa

Browse files
fix: resolve cyclic structure serialization crash in token counter
When the token count API call fails or is not available, the CLI fallback logic serializes using . However, contains raw Zod schema objects (), which have circular references, causing a TypeError. Changes: - Added a utility to strip circular references from objects. - Sanitize the inside at creation time using so it is a plain, serializable object. This also prevents similar serialization failures when saving run state to disk.
1 parent 088a57e commit 85753fa

2 files changed

Lines changed: 27 additions & 3 deletions

File tree

packages/agent-runtime/src/run-agent-step.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
buildUserMessageContent,
4646
expireMessages,
4747
} from './util/messages'
48-
import { countTokensJson } from './util/token-counter'
48+
import { countTokensJson, safeJsonStringify } from './util/token-counter'
4949

5050
import type { AgentTemplate } from '@codebuff/common/types/agent-template'
5151
import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
@@ -860,7 +860,7 @@ export async function loopAgentSteps(
860860
// Convert tools to a serializable format for context-pruner token counting
861861
const toolDefinitions = mapValues(tools, (tool) => ({
862862
description: tool.description,
863-
inputSchema: tool.inputSchema as {},
863+
inputSchema: JSON.parse(safeJsonStringify(tool.inputSchema)),
864864
}))
865865

866866
const additionalToolDefinitionsWithCache = async () => {

packages/agent-runtime/src/util/token-counter.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,34 @@ export function countTokens(text: string): number {
2727
}
2828
}
2929

30+
export function safeJsonStringify(obj: any): string {
31+
const seen = new WeakSet()
32+
return JSON.stringify(obj, (key, value) => {
33+
if (typeof value === 'object' && value !== null) {
34+
if (seen.has(value)) {
35+
return '[Circular]'
36+
}
37+
seen.add(value)
38+
}
39+
if (typeof value === 'function') {
40+
return value.toString()
41+
}
42+
return value
43+
})
44+
}
45+
3046
export function countTokensJson(text: string | object): number {
31-
return countTokens(JSON.stringify(text))
47+
if (typeof text === 'string') {
48+
return countTokens(text)
49+
}
50+
try {
51+
return countTokens(JSON.stringify(text))
52+
} catch (e) {
53+
return countTokens(safeJsonStringify(text))
54+
}
3255
}
3356

57+
3458
export function countTokensForFiles(
3559
files: Record<string, string | null>,
3660
): Record<string, number> {

0 commit comments

Comments
 (0)