File tree Expand file tree Collapse file tree
packages/agent-runtime/src Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -57,6 +57,7 @@ import {
5757 countTokens ,
5858 countTokensJson ,
5959 countTokensMessages ,
60+ safeJsonStringify ,
6061} from './util/token-counter'
6162
6263import type { AgentTemplate } from '@codebuff/common/types/agent-template'
@@ -940,7 +941,7 @@ export async function loopAgentSteps(
940941 const toolDefinitions = mapValues ( tools , ( tool ) => ( {
941942 description :
942943 typeof tool . description === 'string' ? tool . description : undefined ,
943- inputSchema : tool . inputSchema as { } ,
944+ inputSchema : JSON . parse ( safeJsonStringify ( tool . inputSchema ) ?? 'null' ) ,
944945 } ) )
945946
946947 const additionalToolDefinitionsWithCache = async ( ) => {
Original file line number Diff line number Diff line change @@ -4,10 +4,33 @@ import {
44 countTokens ,
55 countTokensJson ,
66 countTokensMessages ,
7+ safeJsonStringify ,
78} from '../token-counter'
89
910import type { Message } from '@codebuff/common/types/messages/codebuff-message'
1011
12+ describe ( 'safeJsonStringify' , ( ) => {
13+ test ( 'handles circular references and drops function properties' , ( ) => {
14+ const schema : { name : string ; self ?: unknown ; transform ?: ( ) => void } = {
15+ name : 'tool input' ,
16+ transform : ( ) => { } ,
17+ }
18+ schema . self = schema
19+
20+ expect ( safeJsonStringify ( schema ) ) . toBe (
21+ '{"name":"tool input","self":"[Circular]"}' ,
22+ )
23+ expect ( ( ) => countTokensJson ( schema ) ) . not . toThrow ( )
24+ expect ( countTokensJson ( schema ) ) . toBeGreaterThan ( 0 )
25+ } )
26+
27+ test ( 'preserves JSON string serialization when counting strings' , ( ) => {
28+ expect ( countTokensJson ( 'tool input' ) ) . toBe (
29+ countTokens ( JSON . stringify ( 'tool input' ) ) ,
30+ )
31+ } )
32+ } )
33+
1134describe ( 'countTokensMessages' , ( ) => {
1235 test ( 'counts text content plus per-message overhead' , ( ) => {
1336 const messages = [
Original file line number Diff line number Diff line change @@ -38,10 +38,27 @@ export function countTokens(text: string): number {
3838 }
3939}
4040
41+ /**
42+ * Serialize arbitrary values for token counting and persisted tool metadata.
43+ * Circular references are represented explicitly; functions follow
44+ * JSON.stringify semantics and are omitted from object properties.
45+ */
46+ export function safeJsonStringify ( value : unknown ) : string | undefined {
47+ const seen = new WeakSet < object > ( )
48+ return JSON . stringify ( value , ( _key , nestedValue ) => {
49+ if ( typeof nestedValue === 'function' ) return undefined
50+ if ( typeof nestedValue === 'object' && nestedValue !== null ) {
51+ if ( seen . has ( nestedValue ) ) return '[Circular]'
52+ seen . add ( nestedValue )
53+ }
54+ return nestedValue
55+ } )
56+ }
57+
4158export function countTokensJson ( value : unknown ) : number {
4259 // JSON.stringify(undefined) returns undefined; fall back to '' so countTokens
4360 // always gets a string.
44- return countTokens ( JSON . stringify ( value ) ?? '' )
61+ return countTokens ( safeJsonStringify ( value ) ?? '' )
4562}
4663
4764/**
@@ -95,6 +112,7 @@ export function countTokensMessages(messages: Message[]): number {
95112 return total
96113}
97114
115+
98116export function countTokensForFiles (
99117 files : Record < string , string | null > ,
100118) : Record < string , number > {
You can’t perform that action at this time.
0 commit comments