@@ -12,7 +12,7 @@ import type {
1212import type { Ctx } from '../types.js' ;
1313import type { ToolDefinition } from './types.js' ;
1414import { adaptHandler } from './types.js' ;
15- import type { TurnReference , WireTurn } from '../context/wire-context.js' ;
15+ import type { SearchMatch , TurnReference , WireTurn } from '../context/wire-context.js' ;
1616import {
1717 getCurrentSessionWirePath ,
1818 findAllWorkspaceSessions ,
@@ -25,7 +25,13 @@ import {
2525import fs from 'fs' ;
2626import path from 'path' ;
2727import crypto from 'crypto' ;
28- import { DEFAULT_CLUSTER_GAP_SECONDS } from '../config.js' ;
28+ import {
29+ DEFAULT_CLUSTER_GAP_SECONDS ,
30+ DEFAULT_SEARCH_OUTPUT_BUDGET ,
31+ SEARCH_USER_MAX_LEN ,
32+ SEARCH_AGENT_MAX_LEN ,
33+ SEARCH_SNIPPET_MAX_LEN ,
34+ } from '../config.js' ;
2935import { toolResult } from '../utils/tools.js' ;
3036
3137export async function buildWorkspaceContext (
@@ -94,6 +100,97 @@ export function createContextTools(ctx: Ctx): ToolDefinition[] {
94100 return DEFAULT_CLUSTER_GAP_SECONDS ;
95101 }
96102
103+ function resolveDetail ( args : SearchContextArgs ) : 'compact' | 'normal' | 'full' {
104+ const valid : Array < 'compact' | 'normal' | 'full' > = [ 'compact' , 'normal' , 'full' ] ;
105+ if ( typeof args . detail === 'string' && valid . includes ( args . detail as 'compact' | 'normal' | 'full' ) ) {
106+ return args . detail as 'compact' | 'normal' | 'full' ;
107+ }
108+ return 'normal' ;
109+ }
110+
111+ function resolveMaxOutputChars ( args : SearchContextArgs , detail : 'compact' | 'normal' | 'full' ) : number {
112+ if ( detail === 'full' ) return Number . MAX_SAFE_INTEGER ;
113+ if ( typeof args . max_output_chars === 'number' && args . max_output_chars > 0 ) {
114+ return Math . max ( 500 , Math . floor ( args . max_output_chars ) ) ;
115+ }
116+ return DEFAULT_SEARCH_OUTPUT_BUDGET ;
117+ }
118+
119+ function estimateOutputSize ( obj : unknown ) : number {
120+ return JSON . stringify ( obj ) . length ;
121+ }
122+
123+ interface CompactMatch {
124+ sessionId : string ;
125+ turnId : number ;
126+ timestamp : string | null ;
127+ score : number ;
128+ }
129+
130+ interface CompactCluster {
131+ sessionId : string ;
132+ hitTurnId : number ;
133+ memberCount : number ;
134+ refinedCount : number ;
135+ }
136+
137+ interface NormalCluster extends CompactCluster {
138+ members : Array < { sessionId : string ; turnId : number } > ;
139+ }
140+
141+ function buildCompactMatch ( match : SearchMatch ) : CompactMatch {
142+ return {
143+ sessionId : match . sessionId ,
144+ turnId : match . turnId ,
145+ timestamp : match . timestamp ,
146+ score : match . score ,
147+ } ;
148+ }
149+
150+ function buildNormalCluster ( cluster : Cluster , refinedCount : number ) : NormalCluster {
151+ return {
152+ sessionId : cluster . sessionId ,
153+ hitTurnId : cluster . hitTurnId ,
154+ memberCount : cluster . members . length ,
155+ refinedCount,
156+ members : cluster . members ,
157+ } ;
158+ }
159+
160+ function buildCompactCluster ( cluster : Cluster , refinedCount : number ) : CompactCluster {
161+ return {
162+ sessionId : cluster . sessionId ,
163+ hitTurnId : cluster . hitTurnId ,
164+ memberCount : cluster . members . length ,
165+ refinedCount,
166+ } ;
167+ }
168+
169+ function trimOutputToBudget (
170+ result : {
171+ matches : SearchMatch [ ] | CompactMatch [ ] ;
172+ clusters : NormalCluster [ ] | CompactCluster [ ] ;
173+ } ,
174+ maxChars : number ,
175+ ) : void {
176+ // Matches are already sorted by score descending; drop lowest-scoring last.
177+ while ( estimateOutputSize ( result ) > maxChars && result . matches . length > 0 ) {
178+ result . matches . pop ( ) ;
179+ }
180+ while ( estimateOutputSize ( result ) > maxChars && result . clusters . length > 0 ) {
181+ result . clusters . pop ( ) ;
182+ }
183+ // If still over budget, strip members arrays from clusters.
184+ if ( estimateOutputSize ( result ) > maxChars && result . clusters . length > 0 ) {
185+ result . clusters = result . clusters . map ( ( c ) => ( {
186+ sessionId : c . sessionId ,
187+ hitTurnId : c . hitTurnId ,
188+ memberCount : c . memberCount ,
189+ refinedCount : c . refinedCount ,
190+ } ) ) as CompactCluster [ ] ;
191+ }
192+ }
193+
97194 function groupHitsIntoBlocks ( hits : WireTurn [ ] ) : WireTurn [ ] [ ] {
98195 const sorted = [ ...hits ] . sort (
99196 ( a , b ) => parseInt ( a . turnId , 10 ) - parseInt ( b . turnId , 10 ) ,
@@ -228,12 +325,26 @@ export function createContextTools(ctx: Ctx): ToolDefinition[] {
228325 typeof args . max_cluster_size === 'number' && args . max_cluster_size > 0
229326 ? Math . max ( 2 , Math . floor ( args . max_cluster_size ) )
230327 : 15 ;
231- const { matches, hits, skippedSessionIds } = await searchWireContext ( query , {
328+ const detail = resolveDetail ( args ) ;
329+ const maxOutputChars = resolveMaxOutputChars ( args , detail ) ;
330+
331+ const searchOptions : Parameters < typeof searchWireContext > [ 1 ] = {
232332 limit : args . limit ,
233333 dateFrom : args . date_from ,
234334 dateTo : args . date_to ,
235335 refinedManager,
236- } ) ;
336+ } ;
337+ if ( detail === 'full' ) {
338+ searchOptions . userMaxLen = Number . MAX_SAFE_INTEGER ;
339+ searchOptions . agentMaxLen = Number . MAX_SAFE_INTEGER ;
340+ searchOptions . snippetMaxLen = SEARCH_SNIPPET_MAX_LEN ;
341+ } else if ( detail === 'normal' ) {
342+ searchOptions . userMaxLen = SEARCH_USER_MAX_LEN ;
343+ searchOptions . agentMaxLen = SEARCH_AGENT_MAX_LEN ;
344+ searchOptions . snippetMaxLen = SEARCH_SNIPPET_MAX_LEN ;
345+ }
346+
347+ const { matches, hits, skippedSessionIds } = await searchWireContext ( query , searchOptions ) ;
237348
238349 // Build clusters around each hit and refine any unrefined turns in them.
239350 const hitsBySession = new Map < string , WireTurn [ ] > ( ) ;
@@ -327,21 +438,47 @@ export function createContextTools(ctx: Ctx): ToolDefinition[] {
327438 skippedSessions ,
328439 ) ;
329440
441+ const normalClusters = clusters . map ( ( c , i ) => buildNormalCluster ( c , refinedCountByCluster [ i ] || 0 ) ) ;
442+ const compactClusters = clusters . map ( ( c , i ) => buildCompactCluster ( c , refinedCountByCluster [ i ] || 0 ) ) ;
443+ const compactMatches = matches . map ( buildCompactMatch ) ;
444+
445+ if ( detail === 'normal' ) {
446+ const normalResult = {
447+ query,
448+ totalMatches : matches . length ,
449+ refinedCount,
450+ clusterGapSeconds,
451+ maxClusterSize,
452+ skippedSessions,
453+ matches,
454+ clusters : normalClusters ,
455+ } ;
456+ trimOutputToBudget ( normalResult , maxOutputChars ) ;
457+ return toolResult ( normalResult ) ;
458+ }
459+
460+ if ( detail === 'compact' ) {
461+ return toolResult ( {
462+ query,
463+ totalMatches : matches . length ,
464+ refinedCount,
465+ clusterGapSeconds,
466+ maxClusterSize,
467+ skippedSessions,
468+ matches : compactMatches ,
469+ clusters : compactClusters ,
470+ } ) ;
471+ }
472+
330473 return toolResult ( {
331474 query,
332475 totalMatches : matches . length ,
333- matches ,
476+ refinedCount ,
334477 clusterGapSeconds,
335478 maxClusterSize,
336479 skippedSessions,
337- clusters : clusters . map ( ( c , i ) => ( {
338- sessionId : c . sessionId ,
339- hitTurnId : c . hitTurnId ,
340- memberCount : c . members . length ,
341- refinedCount : refinedCountByCluster [ i ] || 0 ,
342- members : c . members ,
343- } ) ) ,
344- refinedCount,
480+ matches,
481+ clusters : normalClusters ,
345482 } ) ;
346483 }
347484
@@ -478,7 +615,7 @@ export function createContextTools(ctx: Ctx): ToolDefinition[] {
478615 {
479616 name : 'search_context' ,
480617 description :
481- ' Search conversation rounds across all workspace session wires by keywords and optional date range.' ,
618+ " Search conversation rounds across all workspace session wires by keywords and optional date range. Default detail: 'normal' keeps output within ~6000 chars. Use detail: 'compact' for a quick overview (no match text, no cluster members). Use detail: 'full' when you need full match text and all cluster members." ,
482619 inputSchema : {
483620 type : 'object' ,
484621 properties : {
@@ -496,6 +633,17 @@ export function createContextTools(ctx: Ctx): ToolDefinition[] {
496633 description :
497634 '单个 cluster 最多包含的 turn 数,防止连续讨论过长时上下文爆炸。默认 15。' ,
498635 } ,
636+ detail : {
637+ type : 'string' ,
638+ enum : [ 'compact' , 'normal' , 'full' ] ,
639+ description :
640+ "Output detail level. 'normal' (default) returns truncated text and cluster members within the output budget. 'compact' returns only references/counts. 'full' disables the budget and returns longer text." ,
641+ } ,
642+ max_output_chars : {
643+ type : 'number' ,
644+ description :
645+ 'Maximum output length in characters for normal mode. Default 6000. Ignored in compact/full.' ,
646+ } ,
499647 } ,
500648 required : [ 'query' ] ,
501649 } ,
0 commit comments