1+ import { hub as hubNs } from '@evomap/evolver-core' ;
2+ import { AuthError , HubClientError , HubUnreachableError } from './hubFetch.js' ;
3+ const MAX_TEXT_LENGTH = 500 ;
4+ const MAX_CAPABILITY_COUNT = 50 ;
5+ export const PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES = hubNs . AGENT_DIRECTORY_MAX_LIMIT * 2 ;
6+ export function publicAgentSearchQuery ( request ) {
7+ const normalized = hubNs . normalizeAgentSearchRequest ( request ) ;
8+ return {
9+ ...( normalized . query ? { q : normalized . query } : { } ) ,
10+ ...( normalized . signals && normalized . signals . length > 0 ? { signals : normalized . signals . join ( ',' ) } : { } ) ,
11+ limit : hubNs . AGENT_DIRECTORY_MAX_LIMIT ,
12+ online_only : normalized . availability === 'offline' ? 'false' : 'true' ,
13+ } ;
14+ }
15+ export function unsupportedPublicAvailability ( availability ) {
16+ if ( availability === 'busy' || availability === 'offline' || availability === 'unknown' ) {
17+ return hubNs . capabilityUnavailable ( `public_hub_availability_${ availability } _not_supported` ) ;
18+ }
19+ return undefined ;
20+ }
21+ export function unsupportedPublicSort ( sort ) {
22+ if ( sort === 'recent' )
23+ return hubNs . capabilityUnavailable ( 'public_hub_sort_recent_not_supported' ) ;
24+ return undefined ;
25+ }
26+ export function publicTaskDiscoveryQuery ( request ) {
27+ return publicAgentSearchQuery ( hubNs . normalizeAgentTaskDiscoveryRequest ( request ) ) ;
28+ }
29+ export function parsePublicAgentPage ( body ) {
30+ const record = asRecord ( body ) ;
31+ const payload = asRecord ( record ?. [ 'payload' ] ) ?? record ;
32+ const rawItems = Array . isArray ( payload ?. [ 'results' ] )
33+ ? payload [ 'results' ]
34+ : Array . isArray ( payload ?. [ 'agents' ] )
35+ ? payload [ 'agents' ]
36+ : Array . isArray ( body )
37+ ? body
38+ : undefined ;
39+ if ( ! rawItems )
40+ return invalidResponse ( 'agent_directory_results_missing' ) ;
41+ const items = [ ] ;
42+ for ( const raw of rawItems ) {
43+ const parsed = parseAgentEntry ( raw ) ;
44+ if ( ! parsed )
45+ return invalidResponse ( 'agent_directory_entry_invalid' ) ;
46+ items . push ( parsed ) ;
47+ }
48+ const nextCursor = optionalString ( payload ?. [ 'next_cursor' ] ?? payload ?. [ 'nextCursor' ] , 256 ) ;
49+ const hasMoreValue = payload ?. [ 'has_more' ] ?? payload ?. [ 'hasMore' ] ;
50+ if ( hasMoreValue !== undefined && typeof hasMoreValue !== 'boolean' )
51+ return invalidResponse ( 'agent_directory_has_more_invalid' ) ;
52+ return { ok : true , value : { items, ...( nextCursor ? { nextCursor } : { } ) , hasMore : typeof hasMoreValue === 'boolean' ? hasMoreValue : Boolean ( nextCursor ) } } ;
53+ }
54+ export function paginatePublicAgentPage ( page , request , maxOffset = hubNs . AGENT_DIRECTORY_MAX_LIMIT ) {
55+ if ( ! page . ok )
56+ return page ;
57+ const normalized = hubNs . normalizeAgentSearchRequest ( request ) ;
58+ const offset = cursorOffset ( normalized . cursor , maxOffset ) ;
59+ const filtered = normalized . availability
60+ ? page . value . items . filter ( ( item ) => item . availability === normalized . availability )
61+ : page . value . items ;
62+ const items = [ ...filtered ] . sort ( agentComparator ( normalized . sort , normalized . order ) ) ;
63+ const slice = items . slice ( offset , offset + normalized . limit ) ;
64+ const nextOffset = offset + slice . length ;
65+ return {
66+ ok : true ,
67+ value : {
68+ items : slice ,
69+ ...( nextOffset < items . length ? { nextCursor : `offset:${ nextOffset } ` } : { } ) ,
70+ hasMore : nextOffset < items . length ,
71+ } ,
72+ } ;
73+ }
74+ export function mergePublicAgentPages ( pages ) {
75+ const failed = pages . find ( ( page ) => ! page . ok ) ;
76+ if ( failed && ! failed . ok )
77+ return failed ;
78+ const items = new Map ( ) ;
79+ for ( const page of pages ) {
80+ if ( ! page . ok )
81+ continue ;
82+ for ( const item of page . value . items ) {
83+ const previous = items . get ( item . agentId ) ;
84+ if ( ! previous || ( item . score ?? - 1 ) > ( previous . score ?? - 1 ) )
85+ items . set ( item . agentId , item ) ;
86+ }
87+ }
88+ return { ok : true , value : { items : [ ...items . values ( ) ] , hasMore : false } } ;
89+ }
90+ export function parsePublicAgentProfile ( body ) {
91+ if ( body === null )
92+ return { ok : true , value : null } ;
93+ const record = asRecord ( body ) ;
94+ const payload = asRecord ( record ?. [ 'payload' ] ) ?? record ;
95+ const profile = asRecord ( payload ?. [ 'profile' ] ) ?? payload ;
96+ if ( ! profile )
97+ return invalidResponse ( 'agent_profile_invalid' ) ;
98+ const entry = parseAgentEntry ( profile ) ;
99+ if ( ! entry )
100+ return invalidResponse ( 'agent_profile_invalid' ) ;
101+ const taskStatsCompletedCount = completedTaskCountFromTaskStats ( profile [ 'task_stats' ] ) ;
102+ if ( taskStatsCompletedCount === null )
103+ return invalidResponse ( 'agent_profile_invalid' ) ;
104+ const { score : _score , ...safeProfile } = entry ;
105+ return {
106+ ok : true ,
107+ value : {
108+ ...safeProfile ,
109+ ...( safeProfile . completedTaskCount === undefined && taskStatsCompletedCount !== undefined
110+ ? { completedTaskCount : taskStatsCompletedCount }
111+ : { } ) ,
112+ } ,
113+ } ;
114+ }
115+ export function agentDirectoryFailure ( error ) {
116+ if ( error instanceof hubNs . AgentDirectoryInputError )
117+ return { ok : false , error : { code : 'invalid_request' , retryable : false , message : error . message } } ;
118+ if ( error instanceof DirectoryTimeoutError )
119+ return { ok : false , error : { code : 'timeout' , retryable : true , message : 'agent_directory_timeout' } } ;
120+ if ( error instanceof AuthError )
121+ return { ok : false , error : { code : 'permission_denied' , retryable : false , message : 'agent_directory_permission_denied' } } ;
122+ if ( error instanceof HubClientError && ( error . status === 404 || error . status === 405 || error . status === 501 ) )
123+ return hubNs . capabilityUnavailable ( ) ;
124+ if ( error instanceof HubUnreachableError || isNetworkError ( error ) )
125+ return { ok : false , error : { code : 'hub_unavailable' , retryable : true , message : 'agent_directory_hub_unavailable' } } ;
126+ if ( error instanceof HubClientError )
127+ return { ok : false , error : { code : 'invalid_request' , retryable : false , message : `agent_directory_hub_${ error . status } ` } } ;
128+ return { ok : false , error : { code : 'hub_unavailable' , retryable : true , message : 'agent_directory_request_failed' } } ;
129+ }
130+ export async function withDirectoryTimeout ( operation , timeoutMs ) {
131+ let timer ;
132+ try {
133+ return await Promise . race ( [
134+ operation ,
135+ new Promise ( ( _resolve , reject ) => {
136+ timer = setTimeout ( ( ) => reject ( new DirectoryTimeoutError ( ) ) , timeoutMs ) ;
137+ } ) ,
138+ ] ) ;
139+ }
140+ finally {
141+ if ( timer )
142+ clearTimeout ( timer ) ;
143+ }
144+ }
145+ class DirectoryTimeoutError extends Error {
146+ }
147+ function parseAgentEntry ( value ) {
148+ const record = asRecord ( value ) ;
149+ if ( ! record )
150+ return undefined ;
151+ const agentId = optionalString ( record [ 'agent_id' ] ?? record [ 'agentId' ] ?? record [ 'node_id' ] ?? record [ 'nodeId' ] , 128 ) ;
152+ if ( ! agentId )
153+ return undefined ;
154+ const capabilitiesValue = record [ 'capabilities' ] ;
155+ const capabilities = stringList ( capabilitiesValue === undefined || capabilitiesValue === null ? record [ 'signals' ] : capabilitiesValue ) ;
156+ const domains = stringArray ( record [ 'domains' ] ) ;
157+ if ( capabilities === null || domains === null )
158+ return undefined ;
159+ const availability = availabilityValue ( record [ 'availability' ] , record [ 'online' ] ) ;
160+ const score = optionalBoundedNumber ( record [ 'score' ] , 0 , 1 ) ;
161+ const reputation = optionalBoundedNumber ( record [ 'reputation' ] , 0 , 1_000_000 ) ;
162+ const completedTaskCount = optionalInteger ( record [ 'completed_tasks' ] ?? record [ 'completedTaskCount' ] , 0 ) ;
163+ const lastSeenAt = optionalTimestamp ( record [ 'last_seen_at' ] ?? record [ 'lastSeenAt' ] ) ;
164+ if ( availability === null || score === null || reputation === null || completedTaskCount === null || lastSeenAt === null )
165+ return undefined ;
166+ return {
167+ agentId,
168+ ...( optionalString ( record [ 'display_name' ] ?? record [ 'displayName' ] ?? record [ 'name' ] ?? record [ 'alias' ] , 120 ) ? { displayName : optionalString ( record [ 'display_name' ] ?? record [ 'displayName' ] ?? record [ 'name' ] ?? record [ 'alias' ] , 120 ) } : { } ) ,
169+ ...( optionalString ( record [ 'summary' ] ?? record [ 'description' ] , MAX_TEXT_LENGTH ) ? { summary : optionalString ( record [ 'summary' ] ?? record [ 'description' ] , MAX_TEXT_LENGTH ) } : { } ) ,
170+ ...( capabilities !== undefined ? { capabilities } : { } ) ,
171+ ...( domains !== undefined ? { domains } : { } ) ,
172+ ...( score !== undefined ? { score } : { } ) ,
173+ ...( reputation !== undefined ? { reputation } : { } ) ,
174+ ...( completedTaskCount !== undefined ? { completedTaskCount } : { } ) ,
175+ ...( availability ? { availability } : { } ) ,
176+ ...( lastSeenAt !== undefined ? { lastSeenAt } : { } ) ,
177+ } ;
178+ }
179+ function asRecord ( value ) {
180+ return value && typeof value === 'object' && ! Array . isArray ( value ) ? value : undefined ;
181+ }
182+ function hasOwn ( record , key ) {
183+ return Object . prototype . hasOwnProperty . call ( record , key ) ;
184+ }
185+ function optionalString ( value , maxLength ) {
186+ if ( value === undefined || value === null )
187+ return undefined ;
188+ if ( typeof value !== 'string' )
189+ return undefined ;
190+ const text = value . trim ( ) ;
191+ return text && text . length <= maxLength ? text : undefined ;
192+ }
193+ function stringArray ( value ) {
194+ if ( value === undefined || value === null )
195+ return undefined ;
196+ if ( ! Array . isArray ( value ) || value . length > MAX_CAPABILITY_COUNT )
197+ return null ;
198+ const output = [ ] ;
199+ for ( const item of value ) {
200+ const text = optionalString ( item , 64 ) ;
201+ if ( ! text )
202+ return null ;
203+ if ( ! output . includes ( text ) )
204+ output . push ( text ) ;
205+ }
206+ return output ;
207+ }
208+ function stringList ( value ) {
209+ if ( typeof value === 'string' )
210+ return stringArray ( value . split ( ',' ) . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ) ;
211+ return stringArray ( value ) ;
212+ }
213+ function completedTaskCountFromTaskStats ( value ) {
214+ if ( value === undefined )
215+ return undefined ;
216+ const taskStats = asRecord ( value ) ;
217+ if ( ! taskStats )
218+ return null ;
219+ if ( ! hasOwn ( taskStats , 'completed' ) )
220+ return undefined ;
221+ const completed = taskStats [ 'completed' ] ;
222+ if ( ! Number . isInteger ( completed ) || Number ( completed ) < 0 )
223+ return null ;
224+ return Number ( completed ) ;
225+ }
226+ function availabilityValue ( value , online ) {
227+ if ( value === 'online' || value === 'busy' || value === 'offline' || value === 'unknown' )
228+ return value ;
229+ if ( value !== undefined )
230+ return null ;
231+ if ( online === true )
232+ return 'online' ;
233+ if ( online === false )
234+ return 'offline' ;
235+ if ( online !== undefined )
236+ return null ;
237+ return undefined ;
238+ }
239+ function optionalBoundedNumber ( value , min , max ) {
240+ if ( value === undefined || value === null )
241+ return undefined ;
242+ if ( typeof value !== 'number' || ! Number . isFinite ( value ) || value < min || value > max )
243+ return null ;
244+ return value ;
245+ }
246+ function optionalInteger ( value , min ) {
247+ if ( value === undefined || value === null )
248+ return undefined ;
249+ if ( ! Number . isInteger ( value ) || Number ( value ) < min )
250+ return null ;
251+ return Number ( value ) ;
252+ }
253+ function optionalTimestamp ( value ) {
254+ if ( value === undefined || value === null )
255+ return undefined ;
256+ if ( typeof value === 'number' && Number . isFinite ( value ) && value >= 0 )
257+ return value ;
258+ if ( typeof value === 'string' && value . length <= 64 ) {
259+ const parsed = Date . parse ( value ) ;
260+ return Number . isFinite ( parsed ) ? parsed : null ;
261+ }
262+ return null ;
263+ }
264+ function invalidResponse ( message ) {
265+ return { ok : false , error : { code : 'invalid_response' , retryable : false , message } } ;
266+ }
267+ function cursorOffset ( cursor , maxOffset ) {
268+ if ( ! cursor )
269+ return 0 ;
270+ const match = / ^ o f f s e t : ( \d + ) $ / . exec ( cursor ) ;
271+ const offset = match ? Number ( match [ 1 ] ) : NaN ;
272+ if ( ! Number . isInteger ( offset ) || offset < 0 || offset > maxOffset ) {
273+ throw new hubNs . AgentDirectoryInputError ( 'cursor_invalid' ) ;
274+ }
275+ return offset ;
276+ }
277+ function agentComparator ( sort , order ) {
278+ const direction = order === 'asc' ? 1 : - 1 ;
279+ return ( left , right ) => direction * ( sortValue ( left , sort ) - sortValue ( right , sort ) ) || left . agentId . localeCompare ( right . agentId ) ;
280+ }
281+ function sortValue ( entry , sort ) {
282+ if ( sort === 'reputation' )
283+ return entry . reputation ?? - 1 ;
284+ if ( sort === 'recent' )
285+ return entry . lastSeenAt ?? - 1 ;
286+ if ( sort === 'availability' )
287+ return entry . availability === 'online' ? 3 : entry . availability === 'busy' ? 2 : entry . availability === 'unknown' ? 1 : 0 ;
288+ return entry . score ?? - 1 ;
289+ }
290+ function isNetworkError ( error ) {
291+ const record = asRecord ( error ) ;
292+ return record ?. [ 'name' ] === 'AbortError' || record ?. [ 'name' ] === 'TimeoutError' || record ?. [ 'code' ] === 'HUB_UNREACHABLE' ;
293+ }
0 commit comments