@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.
- package/LICENSE +21 -0
- package/README.md +420 -0
- package/a2_cli.mjs +1576 -0
- package/adapters/index.mjs +79 -0
- package/adapters/openclaw/adapter.mjs +1020 -0
- package/adapters/openclaw/cli.mjs +89 -0
- package/adapters/openclaw/detect.mjs +259 -0
- package/adapters/openclaw/helpers.mjs +827 -0
- package/adapters/openclaw/ws_client.mjs +740 -0
- package/bin/a2-cli.js +8 -0
- package/lib/conversation/policy.mjs +122 -0
- package/lib/conversation/store.mjs +223 -0
- package/lib/conversation/templates.mjs +419 -0
- package/lib/gateway/api.mjs +28 -0
- package/lib/gateway/inbox.mjs +344 -0
- package/lib/gateway/lifecycle.mjs +602 -0
- package/lib/gateway/runtime_state.mjs +388 -0
- package/lib/gateway/server.mjs +883 -0
- package/lib/gateway/state.mjs +175 -0
- package/lib/routing/agent_router.mjs +511 -0
- package/lib/runtime/executor.mjs +380 -0
- package/lib/runtime/keys.mjs +85 -0
- package/lib/runtime/report.mjs +302 -0
- package/lib/runtime/safety.mjs +72 -0
- package/lib/shared/paths.mjs +155 -0
- package/lib/shared/primitives.mjs +43 -0
- package/lib/transport/http_json.mjs +96 -0
- package/lib/transport/libp2p.mjs +397 -0
- package/lib/transport/peer_session.mjs +857 -0
- package/lib/transport/relay_http.mjs +110 -0
- package/package.json +53 -0
|
@@ -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
|
+
}
|