@@ -18,6 +18,16 @@ type ComposioExecuteResponse = {
1818 output : ToolResultOutput [ ]
1919}
2020
21+ const COMPOSIO_DISCOVERY_TIMEOUT_MS = 750
22+ const COMPOSIO_DISCOVERY_SUCCESS_CACHE_MS = 5 * 60 * 1000
23+ const COMPOSIO_DISCOVERY_FAILURE_CACHE_MS = 60 * 1000
24+ const COMPOSIO_DISCOVERY_CACHE_MAX_ENTRIES = 64
25+
26+ const composioToolDefinitionsCache = new Map <
27+ string ,
28+ { expiresAt : number ; tools : CustomToolDefinition [ ] }
29+ > ( )
30+
2131function toJsonValue ( value : unknown ) : JSONValue {
2232 try {
2333 return JSON . parse ( JSON . stringify ( value ?? null ) ) as JSONValue
@@ -26,6 +36,70 @@ function toJsonValue(value: unknown): JSONValue {
2636 }
2737}
2838
39+ function createTimeoutSignal (
40+ parentSignal : AbortSignal | undefined ,
41+ timeoutMs : number ,
42+ ) {
43+ const controller = new AbortController ( )
44+ const timeout = setTimeout ( ( ) => {
45+ controller . abort ( new Error ( 'Composio discovery timed out' ) )
46+ } , timeoutMs )
47+ const onAbort = ( ) => controller . abort ( parentSignal ?. reason )
48+
49+ if ( parentSignal ?. aborted ) {
50+ onAbort ( )
51+ } else {
52+ parentSignal ?. addEventListener ( 'abort' , onAbort , { once : true } )
53+ }
54+
55+ return {
56+ signal : controller . signal ,
57+ cleanup : ( ) => {
58+ clearTimeout ( timeout )
59+ parentSignal ?. removeEventListener ( 'abort' , onAbort )
60+ } ,
61+ }
62+ }
63+
64+ function getCachedComposioTools ( apiKey : string ) : CustomToolDefinition [ ] | null {
65+ const cached = composioToolDefinitionsCache . get ( apiKey )
66+ if ( ! cached ) return null
67+ if ( Date . now ( ) >= cached . expiresAt ) {
68+ composioToolDefinitionsCache . delete ( apiKey )
69+ return null
70+ }
71+ return cached . tools
72+ }
73+
74+ function pruneComposioToolsCache ( now : number ) {
75+ for ( const [ apiKey , cached ] of composioToolDefinitionsCache ) {
76+ if ( now >= cached . expiresAt ) {
77+ composioToolDefinitionsCache . delete ( apiKey )
78+ }
79+ }
80+
81+ while (
82+ composioToolDefinitionsCache . size >= COMPOSIO_DISCOVERY_CACHE_MAX_ENTRIES
83+ ) {
84+ const oldest = composioToolDefinitionsCache . keys ( ) . next ( )
85+ if ( oldest . done ) break
86+ composioToolDefinitionsCache . delete ( oldest . value )
87+ }
88+ }
89+
90+ function cacheComposioTools (
91+ apiKey : string ,
92+ tools : CustomToolDefinition [ ] ,
93+ ttlMs : number ,
94+ ) {
95+ const now = Date . now ( )
96+ pruneComposioToolsCache ( now )
97+ composioToolDefinitionsCache . set ( apiKey , {
98+ tools,
99+ expiresAt : now + ttlMs ,
100+ } )
101+ }
102+
29103async function readErrorMessage ( response : Response ) : Promise < string > {
30104 try {
31105 const body = ( await response . json ( ) ) as {
@@ -90,24 +164,41 @@ async function executeComposioToolViaServer(params: {
90164export async function getComposioCustomToolDefinitions ( params : {
91165 apiKey : string
92166 logger ?: Pick < Logger , 'warn' >
167+ signal ?: AbortSignal
93168} ) : Promise < CustomToolDefinition [ ] > {
169+ const cachedTools = getCachedComposioTools ( params . apiKey )
170+ if ( cachedTools ) return cachedTools
171+ if ( params . signal ?. aborted ) return [ ]
172+
173+ const discoverySignal = createTimeoutSignal (
174+ params . signal ,
175+ COMPOSIO_DISCOVERY_TIMEOUT_MS ,
176+ )
177+
94178 let response : Response
95179 try {
96180 response = await fetch ( new URL ( '/api/v1/composio/tools' , WEBSITE_URL ) , {
97181 method : 'POST' ,
98182 headers : {
99183 Authorization : `Bearer ${ params . apiKey } ` ,
100184 } ,
185+ signal : discoverySignal . signal ,
101186 } )
102187 } catch ( error ) {
103- params . logger ?. warn (
104- { error : error instanceof Error ? error . message : String ( error ) } ,
105- 'Failed to fetch Composio tools' ,
106- )
188+ if ( ! params . signal ?. aborted ) {
189+ cacheComposioTools ( params . apiKey , [ ] , COMPOSIO_DISCOVERY_FAILURE_CACHE_MS )
190+ params . logger ?. warn (
191+ { error : error instanceof Error ? error . message : String ( error ) } ,
192+ 'Failed to fetch Composio tools' ,
193+ )
194+ }
107195 return [ ]
196+ } finally {
197+ discoverySignal . cleanup ( )
108198 }
109199
110200 if ( ! response . ok ) {
201+ cacheComposioTools ( params . apiKey , [ ] , COMPOSIO_DISCOVERY_FAILURE_CACHE_MS )
111202 if ( response . status !== 503 ) {
112203 params . logger ?. warn (
113204 { status : response . status , error : await readErrorMessage ( response ) } ,
@@ -119,13 +210,13 @@ export async function getComposioCustomToolDefinitions(params: {
119210
120211 try {
121212 const body = ( await response . json ( ) ) as ComposioToolsResponse
122- return body . tools . map ( ( tool ) => ( {
213+ const tools = body . tools . map ( ( tool ) => ( {
123214 toolName : tool . toolName ,
124215 inputSchema : tool . inputSchema ,
125216 description : tool . description ,
126217 endsAgentStep : true ,
127218 exampleInputs : [ ] ,
128- execute : async ( input ) => {
219+ execute : async ( input : unknown ) => {
129220 return executeComposioToolViaServer ( {
130221 apiKey : params . apiKey ,
131222 sessionId : body . sessionId ,
@@ -137,7 +228,14 @@ export async function getComposioCustomToolDefinitions(params: {
137228 } )
138229 } ,
139230 } ) )
231+ cacheComposioTools (
232+ params . apiKey ,
233+ tools ,
234+ COMPOSIO_DISCOVERY_SUCCESS_CACHE_MS ,
235+ )
236+ return tools
140237 } catch ( error ) {
238+ cacheComposioTools ( params . apiKey , [ ] , COMPOSIO_DISCOVERY_FAILURE_CACHE_MS )
141239 params . logger ?. warn (
142240 { error : error instanceof Error ? error . message : String ( error ) } ,
143241 'Failed to parse Composio tools response' ,
0 commit comments