@agentsquared/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,388 @@
1
+ import { randomRequestId, utcNow } from '../shared/primitives.mjs'
2
+
3
+ const DEFAULT_WAIT_MS = 30000
4
+ const DEFAULT_INBOUND_TIMEOUT_MS = 210 * 1000
5
+ const DEFAULT_PEER_SESSION_TTL_MS = 30 * 60 * 1000
6
+ const DEFAULT_HANDLED_REQUEST_TTL_MS = 6 * 60 * 60 * 1000
7
+ const DEFAULT_MAX_HANDLED_REQUESTS = 512
8
+
9
+ function nowISO() {
10
+ return utcNow()
11
+ }
12
+
13
+ function clone(value) {
14
+ return JSON.parse(JSON.stringify(value))
15
+ }
16
+
17
+ export function createGatewayRuntimeState({
18
+ inboundTimeoutMs = DEFAULT_INBOUND_TIMEOUT_MS,
19
+ peerSessionTTLms = DEFAULT_PEER_SESSION_TTL_MS,
20
+ handledRequestTTLms = DEFAULT_HANDLED_REQUEST_TTL_MS,
21
+ maxHandledRequests = DEFAULT_MAX_HANDLED_REQUESTS
22
+ } = {}) {
23
+ const inboundQueue = []
24
+ const nextWaiters = []
25
+ const pendingInbound = new Map()
26
+ const pendingRequests = new Map()
27
+ const trustedSessions = new Map()
28
+ const trustedByConversation = new Map()
29
+ const handledRequests = new Map()
30
+
31
+ function pruneExpiredInboundQueue() {
32
+ const now = Date.now()
33
+ for (let index = inboundQueue.length - 1; index >= 0; index -= 1) {
34
+ const item = inboundQueue[index]
35
+ if (!item || Date.parse(item.expiresAt) > now) {
36
+ continue
37
+ }
38
+ inboundQueue.splice(index, 1)
39
+ }
40
+ }
41
+
42
+ function releaseNextWaiter() {
43
+ pruneExpiredInboundQueue()
44
+ while (nextWaiters.length > 0) {
45
+ const waiter = nextWaiters.shift()
46
+ if (!waiter || Date.now() > waiter.expiresAt) {
47
+ waiter?.resolve?.(null)
48
+ continue
49
+ }
50
+ const next = inboundQueue.shift() ?? null
51
+ waiter.resolve(next ? clone(next) : null)
52
+ return
53
+ }
54
+ }
55
+
56
+ function pruneExpiredTrustedSessions() {
57
+ const now = Date.now()
58
+ for (const [sessionId, session] of trustedSessions.entries()) {
59
+ if (Date.parse(session.expiresAt) <= now) {
60
+ trustedSessions.delete(sessionId)
61
+ const conversationKey = `${session.conversationKey ?? ''}`.trim()
62
+ if (conversationKey) {
63
+ const knownConversationSessionId = trustedByConversation.get(conversationKey)
64
+ if (knownConversationSessionId === sessionId) {
65
+ trustedByConversation.delete(conversationKey)
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ function handledRequestKey(peerSessionId, requestId) {
73
+ return `${`${peerSessionId ?? ''}`.trim()}::${`${requestId ?? ''}`.trim()}`
74
+ }
75
+
76
+ function pruneExpiredHandledRequests() {
77
+ const now = Date.now()
78
+ for (const [key, entry] of handledRequests.entries()) {
79
+ if (Date.parse(entry.expiresAt) > now) {
80
+ continue
81
+ }
82
+ handledRequests.delete(key)
83
+ }
84
+ }
85
+
86
+ function pruneExpiredInbound() {
87
+ const now = Date.now()
88
+ pruneExpiredInboundQueue()
89
+ pruneExpiredHandledRequests()
90
+ for (const [inboundId, inbound] of pendingInbound.entries()) {
91
+ if (Date.parse(inbound.expiresAt) > now) {
92
+ continue
93
+ }
94
+ inbound.reject(new Error('gateway inbound request timed out before the local runtime responded'))
95
+ if (inbound.requestKey) {
96
+ pendingRequests.delete(inbound.requestKey)
97
+ }
98
+ pendingInbound.delete(inboundId)
99
+ }
100
+ }
101
+
102
+ function rememberHandledRequest({
103
+ peerSessionId,
104
+ requestId,
105
+ response
106
+ }) {
107
+ const normalizedSessionId = `${peerSessionId ?? ''}`.trim()
108
+ const normalizedRequestId = `${requestId ?? ''}`.trim()
109
+ if (!normalizedSessionId || !normalizedRequestId) {
110
+ throw new Error('peerSessionId and requestId are required to remember a handled AgentSquared request')
111
+ }
112
+ pruneExpiredHandledRequests()
113
+ const now = Date.now()
114
+ const entry = {
115
+ peerSessionId: normalizedSessionId,
116
+ requestId: normalizedRequestId,
117
+ response: clone(response),
118
+ createdAt: nowISO(),
119
+ expiresAt: new Date(now + handledRequestTTLms).toISOString()
120
+ }
121
+ handledRequests.set(handledRequestKey(normalizedSessionId, normalizedRequestId), entry)
122
+ while (handledRequests.size > Math.max(32, Number.parseInt(`${maxHandledRequests ?? DEFAULT_MAX_HANDLED_REQUESTS}`, 10) || DEFAULT_MAX_HANDLED_REQUESTS)) {
123
+ const oldestKey = handledRequests.keys().next().value
124
+ if (!oldestKey) {
125
+ break
126
+ }
127
+ handledRequests.delete(oldestKey)
128
+ }
129
+ return clone(entry)
130
+ }
131
+
132
+ function handledRequestResponse(peerSessionId, requestId) {
133
+ pruneExpiredHandledRequests()
134
+ const entry = handledRequests.get(handledRequestKey(peerSessionId, requestId))
135
+ return entry ? clone(entry.response) : null
136
+ }
137
+
138
+ function rememberTrustedSession({
139
+ peerSessionId,
140
+ conversationKey = '',
141
+ remoteAgentId,
142
+ remotePeerId,
143
+ remoteTransport = null,
144
+ ticketView = null,
145
+ skillHint = ''
146
+ }) {
147
+ if (!peerSessionId?.trim() || !remoteAgentId?.trim() || !remotePeerId?.trim()) {
148
+ throw new Error('peerSessionId, remoteAgentId, and remotePeerId are required to remember a trusted peer session')
149
+ }
150
+ pruneExpiredTrustedSessions()
151
+ const now = Date.now()
152
+ const existingSession = trustedSessions.get(peerSessionId.trim())
153
+ const previousConversationKey = `${existingSession?.conversationKey ?? ''}`.trim()
154
+ if (previousConversationKey && previousConversationKey !== `${conversationKey ?? ''}`.trim()) {
155
+ const mappedSessionId = trustedByConversation.get(previousConversationKey)
156
+ if (mappedSessionId === peerSessionId.trim()) {
157
+ trustedByConversation.delete(previousConversationKey)
158
+ }
159
+ }
160
+ const session = {
161
+ peerSessionId: peerSessionId.trim(),
162
+ conversationKey: `${conversationKey ?? ''}`.trim(),
163
+ remoteAgentId: remoteAgentId.trim(),
164
+ remotePeerId: remotePeerId.trim(),
165
+ remoteTransport: remoteTransport ? clone(remoteTransport) : null,
166
+ ticketView: ticketView ? clone(ticketView) : null,
167
+ skillHint: `${skillHint}`.trim(),
168
+ createdAt: nowISO(),
169
+ lastUsedAt: nowISO(),
170
+ expiresAt: new Date(now + peerSessionTTLms).toISOString()
171
+ }
172
+ trustedSessions.set(session.peerSessionId, session)
173
+ if (session.conversationKey) {
174
+ trustedByConversation.set(session.conversationKey, session.peerSessionId)
175
+ }
176
+ return clone(session)
177
+ }
178
+
179
+ function touchTrustedSession(peerSessionId) {
180
+ pruneExpiredTrustedSessions()
181
+ const session = trustedSessions.get(`${peerSessionId}`.trim())
182
+ if (!session) return null
183
+ session.lastUsedAt = nowISO()
184
+ session.expiresAt = new Date(Date.now() + peerSessionTTLms).toISOString()
185
+ if (`${session.conversationKey ?? ''}`.trim()) {
186
+ trustedByConversation.set(session.conversationKey, session.peerSessionId)
187
+ }
188
+ return clone(session)
189
+ }
190
+
191
+ function trustedSessionByConversation(conversationKey) {
192
+ pruneExpiredTrustedSessions()
193
+ const normalizedConversationKey = `${conversationKey ?? ''}`.trim()
194
+ if (!normalizedConversationKey) {
195
+ return null
196
+ }
197
+ const sessionId = trustedByConversation.get(normalizedConversationKey)
198
+ if (!sessionId) {
199
+ return null
200
+ }
201
+ const session = trustedSessions.get(sessionId)
202
+ if (!session) {
203
+ trustedByConversation.delete(normalizedConversationKey)
204
+ return null
205
+ }
206
+ return clone(session)
207
+ }
208
+
209
+ function trustedSessionById(peerSessionId) {
210
+ pruneExpiredTrustedSessions()
211
+ const session = trustedSessions.get(`${peerSessionId}`.trim())
212
+ return session ? clone(session) : null
213
+ }
214
+
215
+ async function enqueueInbound({
216
+ request,
217
+ ticketView = null,
218
+ remotePeerId,
219
+ remoteAgentId,
220
+ peerSessionId,
221
+ suggestedSkill = '',
222
+ defaultSkill = 'friend-im'
223
+ }) {
224
+ pruneExpiredInbound()
225
+ const normalizedPeerSessionId = `${peerSessionId}`.trim()
226
+ const normalizedRequestId = `${request?.id ?? ''}`.trim()
227
+ const requestKey = normalizedPeerSessionId && normalizedRequestId
228
+ ? handledRequestKey(normalizedPeerSessionId, normalizedRequestId)
229
+ : ''
230
+ if (requestKey) {
231
+ const existingPending = pendingRequests.get(requestKey)
232
+ if (existingPending) {
233
+ return {
234
+ inboundId: existingPending.inboundId,
235
+ payload: clone(existingPending.payload),
236
+ responsePromise: existingPending.responsePromise,
237
+ duplicateOfPending: true
238
+ }
239
+ }
240
+ }
241
+ const inboundId = randomRequestId('inbound')
242
+ const payload = {
243
+ inboundId,
244
+ receivedAt: nowISO(),
245
+ expiresAt: new Date(Date.now() + inboundTimeoutMs).toISOString(),
246
+ remotePeerId: `${remotePeerId}`.trim(),
247
+ remoteAgentId: `${remoteAgentId}`.trim(),
248
+ peerSessionId: `${peerSessionId}`.trim(),
249
+ suggestedSkill: `${suggestedSkill}`.trim(),
250
+ defaultSkill: `${defaultSkill}`.trim() || 'friend-im',
251
+ ticketView: ticketView ? clone(ticketView) : null,
252
+ request: clone(request)
253
+ }
254
+ const responsePromise = new Promise((resolve, reject) => {
255
+ pendingInbound.set(inboundId, {
256
+ resolve,
257
+ reject,
258
+ expiresAt: payload.expiresAt,
259
+ requestKey
260
+ })
261
+ })
262
+ if (requestKey) {
263
+ pendingRequests.set(requestKey, {
264
+ inboundId,
265
+ payload: clone(payload),
266
+ responsePromise,
267
+ expiresAt: payload.expiresAt
268
+ })
269
+ }
270
+ inboundQueue.push(payload)
271
+ releaseNextWaiter()
272
+ return {
273
+ inboundId,
274
+ payload,
275
+ responsePromise
276
+ }
277
+ }
278
+
279
+ async function nextInbound({ waitMs = DEFAULT_WAIT_MS } = {}) {
280
+ pruneExpiredInbound()
281
+ if (inboundQueue.length > 0) {
282
+ return clone(inboundQueue.shift())
283
+ }
284
+ const boundedWait = Math.max(0, Number.parseInt(`${waitMs}`, 10) || DEFAULT_WAIT_MS)
285
+ return new Promise((resolve) => {
286
+ const waiter = {
287
+ expiresAt: Date.now() + boundedWait,
288
+ resolve: (value) => {
289
+ clearTimeout(timer)
290
+ resolve(value)
291
+ }
292
+ }
293
+ const expiresAt = Date.now() + boundedWait
294
+ const timer = setTimeout(() => {
295
+ const index = nextWaiters.indexOf(waiter)
296
+ if (index >= 0) {
297
+ nextWaiters.splice(index, 1)
298
+ }
299
+ resolve(null)
300
+ }, boundedWait)
301
+ waiter.expiresAt = expiresAt
302
+ nextWaiters.push(waiter)
303
+ })
304
+ }
305
+
306
+ function respondInbound({ inboundId, result }) {
307
+ pruneExpiredInbound()
308
+ const pending = pendingInbound.get(`${inboundId}`.trim())
309
+ if (!pending) {
310
+ throw new Error(`unknown inboundId: ${inboundId}`)
311
+ }
312
+ pendingInbound.delete(`${inboundId}`.trim())
313
+ if (pending.requestKey) {
314
+ pendingRequests.delete(pending.requestKey)
315
+ }
316
+ pending.resolve(clone(result))
317
+ }
318
+
319
+ function rejectInbound({ inboundId, code = 500, message = 'local runtime rejected the inbound request' }) {
320
+ pruneExpiredInbound()
321
+ const pending = pendingInbound.get(`${inboundId}`.trim())
322
+ if (!pending) {
323
+ throw new Error(`unknown inboundId: ${inboundId}`)
324
+ }
325
+ pendingInbound.delete(`${inboundId}`.trim())
326
+ if (pending.requestKey) {
327
+ pendingRequests.delete(pending.requestKey)
328
+ }
329
+ pending.reject(Object.assign(new Error(`${message}`), { code }))
330
+ }
331
+
332
+ function reset({
333
+ reason = 'gateway runtime state was reset',
334
+ preserveTrustedSessions = false
335
+ } = {}) {
336
+ const error = Object.assign(new Error(`${reason}`), { code: 503 })
337
+ inboundQueue.splice(0, inboundQueue.length)
338
+ while (nextWaiters.length > 0) {
339
+ const waiter = nextWaiters.shift()
340
+ waiter?.resolve?.(null)
341
+ }
342
+ for (const [inboundId, pending] of pendingInbound.entries()) {
343
+ pending.reject(error)
344
+ if (pending.requestKey) {
345
+ pendingRequests.delete(pending.requestKey)
346
+ }
347
+ pendingInbound.delete(inboundId)
348
+ }
349
+ if (!preserveTrustedSessions) {
350
+ trustedSessions.clear()
351
+ trustedByConversation.clear()
352
+ handledRequests.clear()
353
+ pendingRequests.clear()
354
+ return
355
+ }
356
+ pruneExpiredTrustedSessions()
357
+ pruneExpiredHandledRequests()
358
+ }
359
+
360
+ return {
361
+ rememberTrustedSession,
362
+ touchTrustedSession,
363
+ trustedSessionByConversation,
364
+ trustedSessionById,
365
+ rememberHandledRequest,
366
+ handledRequestResponse,
367
+ enqueueInbound,
368
+ nextInbound,
369
+ respondInbound,
370
+ rejectInbound,
371
+ reset,
372
+ snapshot() {
373
+ pruneExpiredTrustedSessions()
374
+ pruneExpiredInbound()
375
+ return {
376
+ queuedInbound: inboundQueue.length,
377
+ pendingInbound: pendingInbound.size,
378
+ pendingRequests: pendingRequests.size,
379
+ trustedSessions: [...trustedSessions.values()].map((session) => clone(session)),
380
+ trustedByConversation: [...trustedByConversation.entries()].map(([conversationKey, peerSessionId]) => ({
381
+ conversationKey,
382
+ peerSessionId
383
+ })),
384
+ handledRequests: handledRequests.size
385
+ }
386
+ }
387
+ }
388
+ }