@@ -8,12 +8,16 @@ import { formatUntrustedData } from "../../tool.js";
88import { checkIndexUsage } from "../../../helpers/indexCheck.js" ;
99import { type Document , EJSON } from "bson" ;
1010import { ErrorCodes , MongoDBError } from "../../../common/errors.js" ;
11- import { iterateCursorUntilMaxBytes } from "../../../helpers/iterateCursor .js" ;
11+ import { collectCursorUntilMaxBytesLimit } from "../../../helpers/collectCursorUntilMaxBytes .js" ;
1212import { operationWithFallback } from "../../../helpers/operationWithFallback.js" ;
13- import { AGG_COUNT_MAX_TIME_MS_CAP } from "../../../helpers/constants.js" ;
13+ import { AGG_COUNT_MAX_TIME_MS_CAP , ONE_MB , CURSOR_LIMITS_TO_LLM_TEXT } from "../../../helpers/constants.js" ;
1414
1515export const AggregateArgs = {
1616 pipeline : z . array ( z . object ( { } ) . passthrough ( ) ) . describe ( "An array of aggregation stages to execute" ) ,
17+ responseBytesLimit : z . number ( ) . optional ( ) . default ( ONE_MB ) . describe ( `\
18+ The maximum number of bytes to return in the response. This value is capped by the server’s configured maxBytesPerQuery and cannot be exceeded. \
19+ Note to LLM: If the entire aggregation result is required, use the "export" tool instead of increasing this limit.\
20+ ` ) ,
1721} ;
1822
1923export class AggregateTool extends MongoDBToolBase {
@@ -26,7 +30,7 @@ export class AggregateTool extends MongoDBToolBase {
2630 public operationType : OperationType = "read" ;
2731
2832 protected async execute (
29- { database, collection, pipeline } : ToolArgs < typeof this . argsShape > ,
33+ { database, collection, pipeline, responseBytesLimit } : ToolArgs < typeof this . argsShape > ,
3034 { signal } : ToolExecutionContext
3135 ) : Promise < CallToolResult > {
3236 let aggregationCursor : AggregationCursor | undefined ;
@@ -50,29 +54,36 @@ export class AggregateTool extends MongoDBToolBase {
5054 }
5155 aggregationCursor = provider . aggregate ( database , collection , cappedResultsPipeline ) ;
5256
53- const [ totalDocuments , documents ] = await Promise . all ( [
57+ const [ totalDocuments , cursorResults ] = await Promise . all ( [
5458 this . countAggregationResultDocuments ( { provider, database, collection, pipeline } ) ,
55- iterateCursorUntilMaxBytes ( {
59+ collectCursorUntilMaxBytesLimit ( {
5660 cursor : aggregationCursor ,
57- maxBytesPerQuery : this . config . maxBytesPerQuery ,
61+ configuredMaxBytesPerQuery : this . config . maxBytesPerQuery ,
62+ toolResponseBytesLimit : responseBytesLimit ,
5863 abortSignal : signal ,
5964 } ) ,
6065 ] ) ;
6166
62- let messageDescription = `\
63- The aggregation resulted in ${ totalDocuments === undefined ? "indeterminable number of" : totalDocuments } documents.\
64- ` ;
65- if ( documents . length ) {
66- messageDescription += ` \
67- Returning ${ documents . length } documents while respecting the applied limits. \
68- Note to LLM: If entire aggregation result is needed then use "export" tool to export the aggregation results.\
69- ` ;
70- }
67+ // If the total number of documents that the aggregation would've
68+ // resulted in would be greater than the configured
69+ // maxDocumentsPerQuery then we know for sure that the results were
70+ // capped.
71+ const aggregationResultsCappedByMaxDocumentsLimit =
72+ this . config . maxDocumentsPerQuery > 0 &&
73+ ! ! totalDocuments &&
74+ totalDocuments > this . config . maxDocumentsPerQuery ;
7175
7276 return {
7377 content : formatUntrustedData (
74- messageDescription ,
75- documents . length > 0 ? EJSON . stringify ( documents ) : undefined
78+ this . generateMessage ( {
79+ aggResultsCount : totalDocuments ,
80+ documents : cursorResults . documents ,
81+ appliedLimits : [
82+ aggregationResultsCappedByMaxDocumentsLimit ? "config.maxDocumentsPerQuery" : undefined ,
83+ cursorResults . cappedBy ,
84+ ] . filter ( ( limit ) : limit is keyof typeof CURSOR_LIMITS_TO_LLM_TEXT => ! ! limit ) ,
85+ } ) ,
86+ cursorResults . documents . length > 0 ? EJSON . stringify ( cursorResults . documents ) : undefined
7687 ) ,
7788 } ;
7889 } finally {
@@ -132,4 +143,26 @@ Note to LLM: If entire aggregation result is needed then use "export" tool to ex
132143 return totalDocuments ;
133144 } , undefined ) ;
134145 }
146+
147+ private generateMessage ( {
148+ aggResultsCount,
149+ documents,
150+ appliedLimits,
151+ } : {
152+ aggResultsCount : number | undefined ;
153+ documents : unknown [ ] ;
154+ appliedLimits : ( keyof typeof CURSOR_LIMITS_TO_LLM_TEXT ) [ ] ;
155+ } ) : string {
156+ const appliedLimitText = appliedLimits . length
157+ ? `\
158+ while respecting the applied limits of ${ appliedLimits . map ( ( limit ) => CURSOR_LIMITS_TO_LLM_TEXT [ limit ] ) . join ( ", " ) } . \
159+ Note to LLM: If the entire query result is required then use "export" tool to export the query results.\
160+ `
161+ : "" ;
162+
163+ return `\
164+ The aggregation resulted in ${ aggResultsCount === undefined ? "indeterminable number of" : aggResultsCount } documents. \
165+ Returning ${ documents . length } documents${ appliedLimitText ? ` ${ appliedLimitText } ` : "." } \
166+ ` ;
167+ }
135168}
0 commit comments