@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,1020 @@
|
|
|
1
|
+
import { withOpenClawGatewayClient } from './ws_client.mjs'
|
|
2
|
+
import { buildReceiverBaseReport, inferOwnerFacingLanguage, parseAgentSquaredOutboundEnvelope } from '../../lib/conversation/templates.mjs'
|
|
3
|
+
import { normalizeConversationControl, resolveInboundConversationIdentity, resolveSkillMaxTurns } from '../../lib/conversation/policy.mjs'
|
|
4
|
+
import { scrubOutboundText } from '../../lib/runtime/safety.mjs'
|
|
5
|
+
import {
|
|
6
|
+
buildOpenClawConversationSummaryPrompt,
|
|
7
|
+
buildOpenClawLocalSkillInventoryPrompt,
|
|
8
|
+
buildOpenClawOutboundSkillDecisionPrompt,
|
|
9
|
+
buildOpenClawSafetyPrompt,
|
|
10
|
+
buildOpenClawTaskPrompt,
|
|
11
|
+
formatOpenClawLocalSkillInventoryForPrompt,
|
|
12
|
+
latestAssistantText,
|
|
13
|
+
normalizeOpenClawSafetySessionKey,
|
|
14
|
+
normalizeOpenClawSessionKey,
|
|
15
|
+
normalizeSessionList,
|
|
16
|
+
ownerReportText,
|
|
17
|
+
parseOpenClawLocalSkillInventoryResult,
|
|
18
|
+
parseOpenClawSkillDecisionResult,
|
|
19
|
+
parseOpenClawSafetyResult,
|
|
20
|
+
parseOpenClawConversationSummaryResult,
|
|
21
|
+
parseOpenClawTaskResult,
|
|
22
|
+
peerResponseText,
|
|
23
|
+
readOpenClawRunId,
|
|
24
|
+
readOpenClawStatus,
|
|
25
|
+
resolveOwnerRouteFromSessions,
|
|
26
|
+
stableId
|
|
27
|
+
} from './helpers.mjs'
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
buildOpenClawConversationSummaryPrompt,
|
|
31
|
+
buildOpenClawLocalSkillInventoryPrompt,
|
|
32
|
+
buildOpenClawOutboundSkillDecisionPrompt,
|
|
33
|
+
buildOpenClawSafetyPrompt,
|
|
34
|
+
buildOpenClawTaskPrompt,
|
|
35
|
+
parseOpenClawLocalSkillInventoryResult,
|
|
36
|
+
parseOpenClawConversationSummaryResult,
|
|
37
|
+
parseOpenClawSkillDecisionResult,
|
|
38
|
+
parseOpenClawTaskResult
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clean(value) {
|
|
42
|
+
return `${value ?? ''}`.trim()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function randomId(prefix = 'a2') {
|
|
46
|
+
return `${clean(prefix) || 'a2'}-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nowMs() {
|
|
50
|
+
return Date.now()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function localOwnerTimeZone() {
|
|
54
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toNumber(value) {
|
|
58
|
+
const parsed = Number.parseInt(`${value ?? ''}`, 10)
|
|
59
|
+
return Number.isFinite(parsed) ? parsed : 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function excerpt(text, maxLength = 140) {
|
|
63
|
+
const compact = clean(text).replace(/\s+/g, ' ').trim()
|
|
64
|
+
if (!compact) {
|
|
65
|
+
return ''
|
|
66
|
+
}
|
|
67
|
+
return compact.length > maxLength ? `${compact.slice(0, maxLength - 3)}...` : compact
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildReceiverTurnOutline(turns = [], expectedTurnCount = 1) {
|
|
71
|
+
const normalizedTurns = Array.isArray(turns) ? turns : []
|
|
72
|
+
const turnMap = new Map()
|
|
73
|
+
let maxSeenTurnIndex = 0
|
|
74
|
+
for (const turn of normalizedTurns) {
|
|
75
|
+
const turnIndex = Number.parseInt(`${turn?.turnIndex ?? 0}`, 10) || 0
|
|
76
|
+
if (turnIndex > 0) {
|
|
77
|
+
maxSeenTurnIndex = Math.max(maxSeenTurnIndex, turnIndex)
|
|
78
|
+
turnMap.set(turnIndex, turn)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const maxTurnCount = Math.max(1, Number.parseInt(`${expectedTurnCount ?? 1}`, 10) || 1, maxSeenTurnIndex)
|
|
82
|
+
return Array.from({ length: maxTurnCount }, (_, index) => {
|
|
83
|
+
const displayTurnIndex = index + 1
|
|
84
|
+
const turn = turnMap.get(displayTurnIndex)
|
|
85
|
+
if (!turn) {
|
|
86
|
+
return {
|
|
87
|
+
turnIndex: displayTurnIndex,
|
|
88
|
+
summary: 'Earlier turn details were not preserved in the current live transcript, but this conversation continued.'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const inbound = excerpt(turn.inboundText)
|
|
92
|
+
const reply = excerpt(turn.replyText)
|
|
93
|
+
const isFinalTurn = Boolean(turn.finalize) || ['done', 'handoff'].includes(clean(turn.decision).toLowerCase())
|
|
94
|
+
return {
|
|
95
|
+
turnIndex: displayTurnIndex,
|
|
96
|
+
summary: [
|
|
97
|
+
inbound ? `remote said "${inbound}"` : 'remote sent a message',
|
|
98
|
+
reply ? `I replied "${reply}"` : 'I replied',
|
|
99
|
+
isFinalTurn && clean(turn.stopReason) ? `(final stop: ${clean(turn.stopReason)})` : ''
|
|
100
|
+
].filter(Boolean).join(' ')
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function maxTurnIndexFromOutline(turnOutline = []) {
|
|
106
|
+
const normalized = Array.isArray(turnOutline) ? turnOutline : []
|
|
107
|
+
return normalized.reduce((maxSeen, item, index) => {
|
|
108
|
+
const turnIndex = Number.parseInt(`${item?.turnIndex ?? index + 1}`, 10) || (index + 1)
|
|
109
|
+
return Math.max(maxSeen, turnIndex)
|
|
110
|
+
}, 0)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reframeOpenClawAgentError(error, {
|
|
114
|
+
openclawAgent = '',
|
|
115
|
+
localAgentId = ''
|
|
116
|
+
} = {}) {
|
|
117
|
+
const message = clean(error?.message)
|
|
118
|
+
if (!message) {
|
|
119
|
+
return error
|
|
120
|
+
}
|
|
121
|
+
if (message.toLowerCase().includes('unknown agent id')) {
|
|
122
|
+
return new Error(
|
|
123
|
+
`OpenClaw rejected agent id "${clean(openclawAgent)}". AgentSquared needs a real local OpenClaw agent id here, not the AgentSquared id "${clean(localAgentId)}". Configure --openclaw-agent explicitly or make sure OpenClaw exposes a default agent (usually from agents.list[]; fallback is often "main"). Original error: ${message}`
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
return error
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveFinalAssistantResultText({
|
|
130
|
+
waited = null,
|
|
131
|
+
history = null,
|
|
132
|
+
runId = '',
|
|
133
|
+
label = 'OpenClaw run',
|
|
134
|
+
sessionKey = ''
|
|
135
|
+
} = {}) {
|
|
136
|
+
const fromWaited = latestAssistantText(waited, { runId })
|
|
137
|
+
if (clean(fromWaited)) {
|
|
138
|
+
return fromWaited
|
|
139
|
+
}
|
|
140
|
+
const fromHistory = latestAssistantText(history, { runId })
|
|
141
|
+
if (clean(fromHistory)) {
|
|
142
|
+
return fromHistory
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`${clean(label) || 'OpenClaw run'} did not produce a final assistant message for session ${clean(sessionKey) || 'unknown'}.`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function resolveOpenClawOutboundSkillHint({
|
|
148
|
+
localAgentId,
|
|
149
|
+
targetAgentId,
|
|
150
|
+
ownerText,
|
|
151
|
+
openclawAgent = '',
|
|
152
|
+
command = 'openclaw',
|
|
153
|
+
cwd = '',
|
|
154
|
+
configPath = '',
|
|
155
|
+
stateDir = '',
|
|
156
|
+
timeoutMs = 60000,
|
|
157
|
+
gatewayUrl = '',
|
|
158
|
+
gatewayToken = '',
|
|
159
|
+
gatewayPassword = '',
|
|
160
|
+
availableSkills = ['friend-im', 'agent-mutual-learning']
|
|
161
|
+
} = {}) {
|
|
162
|
+
const agentName = clean(openclawAgent)
|
|
163
|
+
if (!agentName) {
|
|
164
|
+
throw new Error(`openclaw agent name is required for ${clean(localAgentId) || 'the local AgentSquared agent'}`)
|
|
165
|
+
}
|
|
166
|
+
return withOpenClawGatewayClient({
|
|
167
|
+
command,
|
|
168
|
+
cwd,
|
|
169
|
+
configPath,
|
|
170
|
+
stateDir,
|
|
171
|
+
gatewayUrl,
|
|
172
|
+
gatewayToken,
|
|
173
|
+
gatewayPassword,
|
|
174
|
+
requestTimeoutMs: timeoutMs
|
|
175
|
+
}, async (client, gatewayContext) => {
|
|
176
|
+
const sessionKey = stableId('agentsquared-outbound-skill-decision', localAgentId, targetAgentId, ownerText)
|
|
177
|
+
const prompt = buildOpenClawOutboundSkillDecisionPrompt({
|
|
178
|
+
localAgentId,
|
|
179
|
+
targetAgentId,
|
|
180
|
+
ownerText,
|
|
181
|
+
availableSkills
|
|
182
|
+
})
|
|
183
|
+
let accepted
|
|
184
|
+
try {
|
|
185
|
+
accepted = await client.request('agent', {
|
|
186
|
+
agentId: agentName,
|
|
187
|
+
sessionKey,
|
|
188
|
+
message: prompt,
|
|
189
|
+
idempotencyKey: stableId('agentsquared-outbound-skill-decision-run', localAgentId, targetAgentId, ownerText)
|
|
190
|
+
}, timeoutMs)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw reframeOpenClawAgentError(error, {
|
|
193
|
+
openclawAgent: agentName,
|
|
194
|
+
localAgentId
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
const runId = readOpenClawRunId(accepted)
|
|
198
|
+
if (!runId) {
|
|
199
|
+
throw new Error('OpenClaw outbound skill decision did not return a runId.')
|
|
200
|
+
}
|
|
201
|
+
const waited = await client.request('agent.wait', {
|
|
202
|
+
runId,
|
|
203
|
+
timeoutMs
|
|
204
|
+
}, timeoutMs + 1000)
|
|
205
|
+
const status = readOpenClawStatus(waited).toLowerCase()
|
|
206
|
+
if (status && status !== 'ok' && status !== 'completed' && status !== 'done') {
|
|
207
|
+
throw new Error(`OpenClaw outbound skill decision returned ${status || 'an unknown status'} for run ${runId}.`)
|
|
208
|
+
}
|
|
209
|
+
const history = await client.request('chat.history', {
|
|
210
|
+
sessionKey,
|
|
211
|
+
limit: 8
|
|
212
|
+
}, timeoutMs)
|
|
213
|
+
const resultText = resolveFinalAssistantResultText({
|
|
214
|
+
waited,
|
|
215
|
+
history,
|
|
216
|
+
runId,
|
|
217
|
+
label: 'OpenClaw outbound skill decision',
|
|
218
|
+
sessionKey
|
|
219
|
+
})
|
|
220
|
+
const parsed = parseOpenClawSkillDecisionResult(resultText, {
|
|
221
|
+
availableSkills,
|
|
222
|
+
defaultSkill: 'friend-im'
|
|
223
|
+
})
|
|
224
|
+
return {
|
|
225
|
+
...parsed,
|
|
226
|
+
openclawRunId: runId,
|
|
227
|
+
openclawSessionKey: sessionKey,
|
|
228
|
+
openclawGatewayUrl: gatewayContext.gatewayUrl
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function summarizeOpenClawConversation({
|
|
234
|
+
localAgentId,
|
|
235
|
+
remoteAgentId,
|
|
236
|
+
selectedSkill = 'friend-im',
|
|
237
|
+
originalOwnerText = '',
|
|
238
|
+
turnLog = [],
|
|
239
|
+
localSkillInventory = '',
|
|
240
|
+
openclawAgent = '',
|
|
241
|
+
command = 'openclaw',
|
|
242
|
+
cwd = '',
|
|
243
|
+
configPath = '',
|
|
244
|
+
stateDir = '',
|
|
245
|
+
timeoutMs = 60000,
|
|
246
|
+
gatewayUrl = '',
|
|
247
|
+
gatewayToken = '',
|
|
248
|
+
gatewayPassword = ''
|
|
249
|
+
} = {}) {
|
|
250
|
+
const agentName = clean(openclawAgent)
|
|
251
|
+
if (!agentName) {
|
|
252
|
+
throw new Error(`openclaw agent name is required for ${clean(localAgentId) || 'the local AgentSquared agent'}`)
|
|
253
|
+
}
|
|
254
|
+
return withOpenClawGatewayClient({
|
|
255
|
+
command,
|
|
256
|
+
cwd,
|
|
257
|
+
configPath,
|
|
258
|
+
stateDir,
|
|
259
|
+
gatewayUrl,
|
|
260
|
+
gatewayToken,
|
|
261
|
+
gatewayPassword,
|
|
262
|
+
requestTimeoutMs: timeoutMs
|
|
263
|
+
}, async (client, gatewayContext) => {
|
|
264
|
+
const sessionKey = stableId(
|
|
265
|
+
'agentsquared-conversation-summary',
|
|
266
|
+
localAgentId,
|
|
267
|
+
remoteAgentId,
|
|
268
|
+
selectedSkill,
|
|
269
|
+
originalOwnerText,
|
|
270
|
+
JSON.stringify(turnLog ?? [])
|
|
271
|
+
)
|
|
272
|
+
const prompt = buildOpenClawConversationSummaryPrompt({
|
|
273
|
+
localAgentId,
|
|
274
|
+
remoteAgentId,
|
|
275
|
+
selectedSkill,
|
|
276
|
+
originalOwnerText,
|
|
277
|
+
turnLog,
|
|
278
|
+
localSkillInventory
|
|
279
|
+
})
|
|
280
|
+
let accepted
|
|
281
|
+
try {
|
|
282
|
+
accepted = await client.request('agent', {
|
|
283
|
+
agentId: agentName,
|
|
284
|
+
sessionKey,
|
|
285
|
+
message: prompt,
|
|
286
|
+
idempotencyKey: stableId(
|
|
287
|
+
'agentsquared-conversation-summary-run',
|
|
288
|
+
localAgentId,
|
|
289
|
+
remoteAgentId,
|
|
290
|
+
selectedSkill,
|
|
291
|
+
originalOwnerText,
|
|
292
|
+
JSON.stringify(turnLog ?? [])
|
|
293
|
+
)
|
|
294
|
+
}, timeoutMs)
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw reframeOpenClawAgentError(error, {
|
|
297
|
+
openclawAgent: agentName,
|
|
298
|
+
localAgentId
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
const runId = readOpenClawRunId(accepted)
|
|
302
|
+
if (!runId) {
|
|
303
|
+
throw new Error('OpenClaw conversation summary did not return a runId.')
|
|
304
|
+
}
|
|
305
|
+
const waited = await client.request('agent.wait', {
|
|
306
|
+
runId,
|
|
307
|
+
timeoutMs
|
|
308
|
+
}, timeoutMs + 1000)
|
|
309
|
+
const status = readOpenClawStatus(waited).toLowerCase()
|
|
310
|
+
if (status && status !== 'ok' && status !== 'completed' && status !== 'done') {
|
|
311
|
+
throw new Error(`OpenClaw conversation summary returned ${status || 'an unknown status'} for run ${runId}.`)
|
|
312
|
+
}
|
|
313
|
+
const history = await client.request('chat.history', {
|
|
314
|
+
sessionKey,
|
|
315
|
+
limit: 8
|
|
316
|
+
}, timeoutMs)
|
|
317
|
+
const resultText = resolveFinalAssistantResultText({
|
|
318
|
+
waited,
|
|
319
|
+
history,
|
|
320
|
+
runId,
|
|
321
|
+
label: 'OpenClaw conversation summary',
|
|
322
|
+
sessionKey
|
|
323
|
+
})
|
|
324
|
+
const parsed = parseOpenClawConversationSummaryResult(resultText)
|
|
325
|
+
return {
|
|
326
|
+
...parsed,
|
|
327
|
+
openclawRunId: runId,
|
|
328
|
+
openclawSessionKey: sessionKey,
|
|
329
|
+
openclawGatewayUrl: gatewayContext.gatewayUrl
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function inspectOpenClawLocalSkills({
|
|
335
|
+
localAgentId,
|
|
336
|
+
openclawAgent = '',
|
|
337
|
+
command = 'openclaw',
|
|
338
|
+
cwd = '',
|
|
339
|
+
configPath = '',
|
|
340
|
+
stateDir = '',
|
|
341
|
+
timeoutMs = 60000,
|
|
342
|
+
gatewayUrl = '',
|
|
343
|
+
gatewayToken = '',
|
|
344
|
+
gatewayPassword = '',
|
|
345
|
+
purpose = 'mutual-learning'
|
|
346
|
+
} = {}) {
|
|
347
|
+
const agentName = clean(openclawAgent)
|
|
348
|
+
if (!agentName) {
|
|
349
|
+
throw new Error(`openclaw agent name is required for ${clean(localAgentId) || 'the local AgentSquared agent'}`)
|
|
350
|
+
}
|
|
351
|
+
return withOpenClawGatewayClient({
|
|
352
|
+
command,
|
|
353
|
+
cwd,
|
|
354
|
+
configPath,
|
|
355
|
+
stateDir,
|
|
356
|
+
gatewayUrl,
|
|
357
|
+
gatewayToken,
|
|
358
|
+
gatewayPassword,
|
|
359
|
+
requestTimeoutMs: timeoutMs
|
|
360
|
+
}, async (client, gatewayContext) => {
|
|
361
|
+
const sessionKey = stableId('agentsquared-local-skill-inventory', localAgentId, purpose)
|
|
362
|
+
const prompt = buildOpenClawLocalSkillInventoryPrompt({
|
|
363
|
+
localAgentId,
|
|
364
|
+
purpose
|
|
365
|
+
})
|
|
366
|
+
let accepted
|
|
367
|
+
try {
|
|
368
|
+
accepted = await client.request('agent', {
|
|
369
|
+
agentId: agentName,
|
|
370
|
+
sessionKey,
|
|
371
|
+
message: prompt,
|
|
372
|
+
idempotencyKey: stableId('agentsquared-local-skill-inventory-run', localAgentId, purpose)
|
|
373
|
+
}, timeoutMs)
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw reframeOpenClawAgentError(error, {
|
|
376
|
+
openclawAgent: agentName,
|
|
377
|
+
localAgentId
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
const runId = readOpenClawRunId(accepted)
|
|
381
|
+
if (!runId) {
|
|
382
|
+
throw new Error('OpenClaw local skill inventory did not return a runId.')
|
|
383
|
+
}
|
|
384
|
+
const waited = await client.request('agent.wait', {
|
|
385
|
+
runId,
|
|
386
|
+
timeoutMs
|
|
387
|
+
}, timeoutMs + 1000)
|
|
388
|
+
const status = readOpenClawStatus(waited).toLowerCase()
|
|
389
|
+
if (status && status !== 'ok' && status !== 'completed' && status !== 'done') {
|
|
390
|
+
throw new Error(`OpenClaw local skill inventory returned ${status || 'an unknown status'} for run ${runId}.`)
|
|
391
|
+
}
|
|
392
|
+
const history = await client.request('chat.history', {
|
|
393
|
+
sessionKey,
|
|
394
|
+
limit: 8
|
|
395
|
+
}, timeoutMs)
|
|
396
|
+
const resultText = resolveFinalAssistantResultText({
|
|
397
|
+
waited,
|
|
398
|
+
history,
|
|
399
|
+
runId,
|
|
400
|
+
label: 'OpenClaw local skill inventory',
|
|
401
|
+
sessionKey
|
|
402
|
+
})
|
|
403
|
+
const parsed = parseOpenClawLocalSkillInventoryResult(resultText)
|
|
404
|
+
return {
|
|
405
|
+
...parsed,
|
|
406
|
+
inventoryPromptText: formatOpenClawLocalSkillInventoryForPrompt(parsed),
|
|
407
|
+
openclawRunId: runId,
|
|
408
|
+
openclawSessionKey: sessionKey,
|
|
409
|
+
openclawGatewayUrl: gatewayContext.gatewayUrl
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function createOpenClawAdapter({
|
|
415
|
+
localAgentId,
|
|
416
|
+
openclawAgent = '',
|
|
417
|
+
conversationStore = null,
|
|
418
|
+
command = 'openclaw',
|
|
419
|
+
cwd = '',
|
|
420
|
+
configPath = '',
|
|
421
|
+
stateDir = '',
|
|
422
|
+
sessionPrefix = 'agentsquared:',
|
|
423
|
+
timeoutMs = 180000,
|
|
424
|
+
gatewayUrl = '',
|
|
425
|
+
gatewayToken = '',
|
|
426
|
+
gatewayPassword = ''
|
|
427
|
+
} = {}) {
|
|
428
|
+
const agentName = clean(openclawAgent)
|
|
429
|
+
if (!agentName) {
|
|
430
|
+
throw new Error(`openclaw agent name is required for ${clean(localAgentId) || 'the local AgentSquared agent'}`)
|
|
431
|
+
}
|
|
432
|
+
const peerBudget = new Map()
|
|
433
|
+
const budgetWindowMs = 10 * 60 * 1000
|
|
434
|
+
const maxWindowTurns = 30
|
|
435
|
+
async function withGateway(fn) {
|
|
436
|
+
return withOpenClawGatewayClient({
|
|
437
|
+
command,
|
|
438
|
+
cwd,
|
|
439
|
+
configPath,
|
|
440
|
+
stateDir,
|
|
441
|
+
gatewayUrl,
|
|
442
|
+
gatewayToken,
|
|
443
|
+
gatewayPassword,
|
|
444
|
+
requestTimeoutMs: timeoutMs
|
|
445
|
+
}, fn)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function listSessions(client) {
|
|
449
|
+
return normalizeSessionList(await client.request('sessions.list', {}, timeoutMs))
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function preflight() {
|
|
453
|
+
return withGateway(async (client, gatewayContext) => {
|
|
454
|
+
const health = await client.request('health', {}, Math.min(timeoutMs, 15000))
|
|
455
|
+
return {
|
|
456
|
+
ok: Boolean(health?.ok),
|
|
457
|
+
gatewayUrl: gatewayContext.gatewayUrl,
|
|
458
|
+
authMode: gatewayContext.authMode,
|
|
459
|
+
health
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function resolveOwnerRoute(client) {
|
|
465
|
+
return resolveOwnerRouteFromSessions(await listSessions(client), {
|
|
466
|
+
agentName
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function readRelationshipSummary(client, sessionKey) {
|
|
471
|
+
if (!clean(sessionKey)) {
|
|
472
|
+
return ''
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const history = await client.request('chat.history', {
|
|
476
|
+
sessionKey,
|
|
477
|
+
limit: 12
|
|
478
|
+
}, timeoutMs)
|
|
479
|
+
return latestAssistantText(history)
|
|
480
|
+
} catch {
|
|
481
|
+
return ''
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function persistRelationshipSummary(client, {
|
|
486
|
+
relationSessionKey,
|
|
487
|
+
remoteAgentId,
|
|
488
|
+
selectedSkill,
|
|
489
|
+
transcript,
|
|
490
|
+
ownerSummary
|
|
491
|
+
} = {}) {
|
|
492
|
+
if (!clean(relationSessionKey) || !clean(ownerSummary)) {
|
|
493
|
+
return null
|
|
494
|
+
}
|
|
495
|
+
const prompt = [
|
|
496
|
+
`You are maintaining long-term AgentSquared relationship memory for local agent ${clean(localAgentId)} about remote agent ${clean(remoteAgentId)}.`,
|
|
497
|
+
`Skill context: ${clean(selectedSkill) || 'friend-im'}`,
|
|
498
|
+
'',
|
|
499
|
+
'Store only a concise long-term summary for future conversations.',
|
|
500
|
+
'Do not preserve raw turn-by-turn detail unless it matters long-term.',
|
|
501
|
+
'Prefer stable facts, collaboration preferences, trust signals, and useful future follow-up notes.',
|
|
502
|
+
'',
|
|
503
|
+
'Latest completed live conversation summary:',
|
|
504
|
+
clean(ownerSummary),
|
|
505
|
+
'',
|
|
506
|
+
'Transcript excerpt from the just-finished live conversation:',
|
|
507
|
+
clean(transcript) || '(none)',
|
|
508
|
+
'',
|
|
509
|
+
'Return one short memory summary.'
|
|
510
|
+
].join('\n')
|
|
511
|
+
const accepted = await client.request('agent', {
|
|
512
|
+
agentId: agentName,
|
|
513
|
+
sessionKey: relationSessionKey,
|
|
514
|
+
message: prompt,
|
|
515
|
+
idempotencyKey: stableId('agentsquared-relationship-memory', localAgentId, remoteAgentId, ownerSummary)
|
|
516
|
+
}, timeoutMs)
|
|
517
|
+
const runId = readOpenClawRunId(accepted)
|
|
518
|
+
if (!runId) {
|
|
519
|
+
return null
|
|
520
|
+
}
|
|
521
|
+
await client.request('agent.wait', {
|
|
522
|
+
runId,
|
|
523
|
+
timeoutMs
|
|
524
|
+
}, timeoutMs + 1000)
|
|
525
|
+
return runId
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function consumePeerBudget({
|
|
529
|
+
remoteAgentId = ''
|
|
530
|
+
} = {}) {
|
|
531
|
+
const key = clean(remoteAgentId).toLowerCase() || 'unknown'
|
|
532
|
+
const currentTime = nowMs()
|
|
533
|
+
const existing = peerBudget.get(key)
|
|
534
|
+
const recentEvents = (existing?.events ?? []).filter((event) => currentTime - event.at <= budgetWindowMs)
|
|
535
|
+
const nextCount = recentEvents.length + 1
|
|
536
|
+
recentEvents.push({ at: currentTime })
|
|
537
|
+
peerBudget.set(key, { events: recentEvents })
|
|
538
|
+
return {
|
|
539
|
+
windowTurns: nextCount,
|
|
540
|
+
overBudget: nextCount > maxWindowTurns
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function executeInbound({
|
|
545
|
+
item,
|
|
546
|
+
selectedSkill,
|
|
547
|
+
mailboxKey
|
|
548
|
+
}) {
|
|
549
|
+
const remoteAgentId = clean(item?.remoteAgentId)
|
|
550
|
+
const incomingSkillHint = clean(item?.suggestedSkill || item?.request?.params?.metadata?.skillHint)
|
|
551
|
+
const receivedAt = new Date().toISOString()
|
|
552
|
+
const inboundText = peerResponseText(item?.request?.params?.message)
|
|
553
|
+
const inboundMetadata = item?.request?.params?.metadata ?? {}
|
|
554
|
+
const parsedEnvelope = parseAgentSquaredOutboundEnvelope(inboundText)
|
|
555
|
+
const displayInboundText = clean(inboundMetadata.originalOwnerText) || clean(parsedEnvelope?.ownerRequest) || inboundText
|
|
556
|
+
const remoteSentAt = clean(inboundMetadata.sentAt) || clean(parsedEnvelope?.sentAt)
|
|
557
|
+
const ownerLanguage = inferOwnerFacingLanguage(displayInboundText, inboundText)
|
|
558
|
+
const ownerTimeZone = localOwnerTimeZone()
|
|
559
|
+
const conversationIdentity = resolveInboundConversationIdentity(item)
|
|
560
|
+
const conversationKey = clean(conversationIdentity.conversationKey)
|
|
561
|
+
return withGateway(async (client, gatewayContext) => {
|
|
562
|
+
const safetySessionKey = normalizeOpenClawSafetySessionKey(localAgentId, remoteAgentId || mailboxKey || 'unknown')
|
|
563
|
+
const safetyPrompt = buildOpenClawSafetyPrompt({
|
|
564
|
+
localAgentId,
|
|
565
|
+
remoteAgentId,
|
|
566
|
+
selectedSkill,
|
|
567
|
+
item
|
|
568
|
+
})
|
|
569
|
+
let safetyAccepted
|
|
570
|
+
try {
|
|
571
|
+
safetyAccepted = await client.request('agent', {
|
|
572
|
+
agentId: agentName,
|
|
573
|
+
sessionKey: safetySessionKey,
|
|
574
|
+
message: safetyPrompt,
|
|
575
|
+
idempotencyKey: `agentsquared-safety-${clean(item?.inboundId) || randomId('inbound')}`
|
|
576
|
+
}, timeoutMs)
|
|
577
|
+
} catch (error) {
|
|
578
|
+
throw reframeOpenClawAgentError(error, {
|
|
579
|
+
openclawAgent: agentName,
|
|
580
|
+
localAgentId
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
const safetyRunId = readOpenClawRunId(safetyAccepted)
|
|
584
|
+
if (!safetyRunId) {
|
|
585
|
+
throw new Error('OpenClaw safety triage did not return a runId.')
|
|
586
|
+
}
|
|
587
|
+
const safetyWaited = await client.request('agent.wait', {
|
|
588
|
+
runId: safetyRunId,
|
|
589
|
+
timeoutMs
|
|
590
|
+
}, timeoutMs + 1000)
|
|
591
|
+
const safetyStatus = readOpenClawStatus(safetyWaited).toLowerCase()
|
|
592
|
+
if (safetyStatus && safetyStatus !== 'ok' && safetyStatus !== 'completed' && safetyStatus !== 'done') {
|
|
593
|
+
throw new Error(`OpenClaw safety triage returned ${safetyStatus || 'an unknown status'} for run ${safetyRunId}.`)
|
|
594
|
+
}
|
|
595
|
+
const safetyHistory = await client.request('chat.history', {
|
|
596
|
+
sessionKey: safetySessionKey,
|
|
597
|
+
limit: 8
|
|
598
|
+
}, timeoutMs)
|
|
599
|
+
const safetyText = latestAssistantText(safetyWaited, { runId: safetyRunId }) || latestAssistantText(safetyHistory, { runId: safetyRunId })
|
|
600
|
+
if (!safetyText) {
|
|
601
|
+
throw new Error(`OpenClaw safety triage did not produce a final assistant message for session ${safetySessionKey}.`)
|
|
602
|
+
}
|
|
603
|
+
const safety = parseOpenClawSafetyResult(safetyText)
|
|
604
|
+
const budget = consumePeerBudget({
|
|
605
|
+
remoteAgentId
|
|
606
|
+
})
|
|
607
|
+
if (budget.overBudget) {
|
|
608
|
+
const peerReplyText = 'I am pausing this AgentSquared request because this peer has reached the recent conversation window limit. My owner can decide whether to continue later.'
|
|
609
|
+
const conversation = normalizeConversationControl(item?.request?.params?.metadata ?? {}, {
|
|
610
|
+
defaultTurnIndex: 1,
|
|
611
|
+
defaultDecision: 'handoff',
|
|
612
|
+
defaultStopReason: 'receiver-budget-limit',
|
|
613
|
+
defaultFinalize: true
|
|
614
|
+
})
|
|
615
|
+
const updatedConversation = conversationStore?.appendTurn?.({
|
|
616
|
+
conversationKey,
|
|
617
|
+
peerSessionId: item?.peerSessionId || '',
|
|
618
|
+
requestId: clean(item?.request?.id),
|
|
619
|
+
remoteAgentId,
|
|
620
|
+
selectedSkill,
|
|
621
|
+
turnIndex: conversation.turnIndex,
|
|
622
|
+
inboundText: displayInboundText,
|
|
623
|
+
replyText: peerReplyText,
|
|
624
|
+
decision: 'handoff',
|
|
625
|
+
stopReason: 'receiver-budget-limit',
|
|
626
|
+
finalize: true,
|
|
627
|
+
ownerSummary: `I paused this exchange because the recent peer conversation window was exceeded. Current 10-minute turn count: ${budget.windowTurns}.`
|
|
628
|
+
}) ?? null
|
|
629
|
+
const ownerReport = buildReceiverBaseReport({
|
|
630
|
+
localAgentId,
|
|
631
|
+
remoteAgentId,
|
|
632
|
+
incomingSkillHint,
|
|
633
|
+
selectedSkill,
|
|
634
|
+
receivedAt,
|
|
635
|
+
inboundText: displayInboundText,
|
|
636
|
+
peerReplyText,
|
|
637
|
+
repliedAt: new Date().toISOString(),
|
|
638
|
+
skillSummary: `I paused this exchange because the recent peer conversation window was exceeded. Current 10-minute turn count: ${budget.windowTurns}.`,
|
|
639
|
+
conversationTurns: updatedConversation?.turns?.length || conversation.turnIndex,
|
|
640
|
+
stopReason: 'receiver-budget-limit',
|
|
641
|
+
detailsAvailableInInbox: true,
|
|
642
|
+
remoteSentAt,
|
|
643
|
+
language: ownerLanguage,
|
|
644
|
+
timeZone: ownerTimeZone,
|
|
645
|
+
localTime: true
|
|
646
|
+
})
|
|
647
|
+
return {
|
|
648
|
+
selectedSkill,
|
|
649
|
+
peerResponse: {
|
|
650
|
+
message: {
|
|
651
|
+
kind: 'message',
|
|
652
|
+
role: 'agent',
|
|
653
|
+
parts: [{ kind: 'text', text: peerReplyText }]
|
|
654
|
+
},
|
|
655
|
+
metadata: {
|
|
656
|
+
selectedSkill,
|
|
657
|
+
runtimeAdapter: 'openclaw',
|
|
658
|
+
conversationKey,
|
|
659
|
+
safetyDecision: 'owner-approval',
|
|
660
|
+
safetyReason: 'peer-conversation-window-exceeded',
|
|
661
|
+
windowTurns: budget.windowTurns,
|
|
662
|
+
turnIndex: conversation.turnIndex,
|
|
663
|
+
decision: 'handoff',
|
|
664
|
+
stopReason: 'receiver-budget-limit',
|
|
665
|
+
finalize: true
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
ownerReport: {
|
|
669
|
+
...ownerReport,
|
|
670
|
+
selectedSkill,
|
|
671
|
+
runtimeAdapter: 'openclaw',
|
|
672
|
+
conversationKey,
|
|
673
|
+
safetyDecision: 'owner-approval',
|
|
674
|
+
safetyReason: 'peer-conversation-window-exceeded',
|
|
675
|
+
windowTurns: budget.windowTurns,
|
|
676
|
+
turnIndex: conversation.turnIndex,
|
|
677
|
+
decision: 'handoff',
|
|
678
|
+
stopReason: 'receiver-budget-limit',
|
|
679
|
+
finalize: true
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (safety.action !== 'allow') {
|
|
684
|
+
const safetyStopReason = 'safety-block'
|
|
685
|
+
const peerReplyText = scrubOutboundText(clean(safety.peerResponse))
|
|
686
|
+
const conversation = normalizeConversationControl(item?.request?.params?.metadata ?? {}, {
|
|
687
|
+
defaultTurnIndex: 1,
|
|
688
|
+
defaultDecision: safety.action === 'owner-approval' ? 'handoff' : 'done',
|
|
689
|
+
defaultStopReason: safetyStopReason,
|
|
690
|
+
defaultFinalize: true
|
|
691
|
+
})
|
|
692
|
+
const updatedConversation = conversationStore?.appendTurn?.({
|
|
693
|
+
conversationKey,
|
|
694
|
+
peerSessionId: item?.peerSessionId || '',
|
|
695
|
+
requestId: clean(item?.request?.id),
|
|
696
|
+
remoteAgentId,
|
|
697
|
+
selectedSkill,
|
|
698
|
+
turnIndex: conversation.turnIndex,
|
|
699
|
+
inboundText: displayInboundText,
|
|
700
|
+
replyText: peerReplyText,
|
|
701
|
+
decision: conversation.decision,
|
|
702
|
+
stopReason: safetyStopReason,
|
|
703
|
+
finalize: true,
|
|
704
|
+
ownerSummary: clean(safety.ownerSummary)
|
|
705
|
+
}) ?? null
|
|
706
|
+
const ownerReport = buildReceiverBaseReport({
|
|
707
|
+
localAgentId,
|
|
708
|
+
remoteAgentId,
|
|
709
|
+
incomingSkillHint,
|
|
710
|
+
selectedSkill,
|
|
711
|
+
receivedAt,
|
|
712
|
+
inboundText: displayInboundText,
|
|
713
|
+
peerReplyText,
|
|
714
|
+
repliedAt: new Date().toISOString(),
|
|
715
|
+
skillSummary: clean(safety.ownerSummary),
|
|
716
|
+
conversationTurns: updatedConversation?.turns?.length || conversation.turnIndex,
|
|
717
|
+
stopReason: safetyStopReason,
|
|
718
|
+
detailsAvailableInInbox: true,
|
|
719
|
+
remoteSentAt,
|
|
720
|
+
language: ownerLanguage,
|
|
721
|
+
timeZone: ownerTimeZone,
|
|
722
|
+
localTime: true
|
|
723
|
+
})
|
|
724
|
+
return {
|
|
725
|
+
selectedSkill,
|
|
726
|
+
peerResponse: {
|
|
727
|
+
message: {
|
|
728
|
+
kind: 'message',
|
|
729
|
+
role: 'agent',
|
|
730
|
+
parts: [{ kind: 'text', text: peerReplyText }]
|
|
731
|
+
},
|
|
732
|
+
metadata: {
|
|
733
|
+
selectedSkill,
|
|
734
|
+
runtimeAdapter: 'openclaw',
|
|
735
|
+
conversationKey,
|
|
736
|
+
safetyDecision: safety.action,
|
|
737
|
+
safetyReason: clean(safety.reason),
|
|
738
|
+
turnIndex: conversation.turnIndex,
|
|
739
|
+
decision: conversation.decision,
|
|
740
|
+
stopReason: safetyStopReason,
|
|
741
|
+
finalize: true
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
ownerReport: {
|
|
745
|
+
...ownerReport,
|
|
746
|
+
selectedSkill,
|
|
747
|
+
runtimeAdapter: 'openclaw',
|
|
748
|
+
conversationKey,
|
|
749
|
+
safetyDecision: safety.action,
|
|
750
|
+
safetyReason: clean(safety.reason),
|
|
751
|
+
turnIndex: conversation.turnIndex,
|
|
752
|
+
decision: conversation.decision,
|
|
753
|
+
stopReason: safetyStopReason,
|
|
754
|
+
finalize: true
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const relationSessionKey = normalizeOpenClawSessionKey(localAgentId, remoteAgentId || mailboxKey || 'unknown', sessionPrefix)
|
|
760
|
+
const inboundConversation = normalizeConversationControl(item?.request?.params?.metadata ?? {}, {
|
|
761
|
+
defaultTurnIndex: 1,
|
|
762
|
+
defaultDecision: 'done',
|
|
763
|
+
defaultStopReason: '',
|
|
764
|
+
defaultFinalize: false
|
|
765
|
+
})
|
|
766
|
+
const metadata = item?.request?.params?.metadata ?? {}
|
|
767
|
+
if (inboundConversation.turnIndex === 1) {
|
|
768
|
+
conversationStore?.endConversation?.(conversationKey)
|
|
769
|
+
}
|
|
770
|
+
const liveConversation = conversationStore?.ensureConversation?.({
|
|
771
|
+
conversationKey,
|
|
772
|
+
peerSessionId: item?.peerSessionId || '',
|
|
773
|
+
remoteAgentId,
|
|
774
|
+
selectedSkill
|
|
775
|
+
}) ?? null
|
|
776
|
+
const conversationTranscript = conversationStore?.transcript?.(liveConversation?.conversationKey || conversationKey) ?? ''
|
|
777
|
+
const relationshipSummary = await readRelationshipSummary(client, relationSessionKey)
|
|
778
|
+
const localSkillMaxTurns = resolveSkillMaxTurns(selectedSkill, metadata?.sharedSkill ?? null)
|
|
779
|
+
const defaultShouldContinue = selectedSkill === 'agent-mutual-learning'
|
|
780
|
+
&& !inboundConversation.finalize
|
|
781
|
+
&& inboundConversation.turnIndex < localSkillMaxTurns
|
|
782
|
+
const sessionKey = stableId(
|
|
783
|
+
'agentsquared-work',
|
|
784
|
+
localAgentId,
|
|
785
|
+
remoteAgentId,
|
|
786
|
+
conversationKey,
|
|
787
|
+
item?.request?.params?.metadata?.turnIndex || '1',
|
|
788
|
+
item?.inboundId
|
|
789
|
+
)
|
|
790
|
+
const prompt = buildOpenClawTaskPrompt({
|
|
791
|
+
localAgentId,
|
|
792
|
+
remoteAgentId,
|
|
793
|
+
selectedSkill,
|
|
794
|
+
item,
|
|
795
|
+
conversationTranscript,
|
|
796
|
+
relationshipSummary,
|
|
797
|
+
senderSkillInventory: clean(metadata?.localSkillInventory)
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
let accepted
|
|
801
|
+
try {
|
|
802
|
+
accepted = await client.request('agent', {
|
|
803
|
+
agentId: agentName,
|
|
804
|
+
sessionKey,
|
|
805
|
+
message: prompt,
|
|
806
|
+
idempotencyKey: `agentsquared-agent-${clean(item?.inboundId) || randomId('inbound')}`
|
|
807
|
+
}, timeoutMs)
|
|
808
|
+
} catch (error) {
|
|
809
|
+
throw reframeOpenClawAgentError(error, {
|
|
810
|
+
openclawAgent: agentName,
|
|
811
|
+
localAgentId
|
|
812
|
+
})
|
|
813
|
+
}
|
|
814
|
+
const runId = readOpenClawRunId(accepted)
|
|
815
|
+
if (!runId) {
|
|
816
|
+
throw new Error('OpenClaw agent call did not return a runId.')
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const waited = await client.request('agent.wait', {
|
|
820
|
+
runId,
|
|
821
|
+
timeoutMs
|
|
822
|
+
}, timeoutMs + 1000)
|
|
823
|
+
const status = readOpenClawStatus(waited).toLowerCase()
|
|
824
|
+
if (status && status !== 'ok' && status !== 'completed' && status !== 'done') {
|
|
825
|
+
throw new Error(`OpenClaw agent.wait returned ${status || 'an unknown status'} for run ${runId}.`)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const history = await client.request('chat.history', {
|
|
829
|
+
sessionKey,
|
|
830
|
+
limit: 12
|
|
831
|
+
}, timeoutMs)
|
|
832
|
+
const resultText = resolveFinalAssistantResultText({
|
|
833
|
+
waited,
|
|
834
|
+
history,
|
|
835
|
+
runId,
|
|
836
|
+
label: 'OpenClaw inbound task',
|
|
837
|
+
sessionKey
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
const parsed = parseOpenClawTaskResult(resultText, {
|
|
841
|
+
defaultSkill: selectedSkill,
|
|
842
|
+
remoteAgentId,
|
|
843
|
+
inboundId: clean(item?.inboundId),
|
|
844
|
+
defaultTurnIndex: inboundConversation.turnIndex,
|
|
845
|
+
defaultDecision: defaultShouldContinue ? 'continue' : 'done',
|
|
846
|
+
defaultStopReason: inboundConversation.finalize ? 'peer-requested-stop' : '',
|
|
847
|
+
defaultFinalize: inboundConversation.finalize ? true : !defaultShouldContinue
|
|
848
|
+
})
|
|
849
|
+
const conversation = normalizeConversationControl(parsed?.peerResponse?.metadata ?? item?.request?.params?.metadata ?? {}, {
|
|
850
|
+
defaultTurnIndex: 1,
|
|
851
|
+
defaultDecision: 'done',
|
|
852
|
+
defaultStopReason: '',
|
|
853
|
+
defaultFinalize: true
|
|
854
|
+
})
|
|
855
|
+
const safePeerReplyText = scrubOutboundText(peerResponseText(parsed.peerResponse))
|
|
856
|
+
const safeOwnerSummary = scrubOutboundText(clean(parsed.ownerReport?.summary))
|
|
857
|
+
const updatedConversation = conversationStore?.appendTurn?.({
|
|
858
|
+
conversationKey,
|
|
859
|
+
peerSessionId: item?.peerSessionId || '',
|
|
860
|
+
requestId: clean(item?.request?.id),
|
|
861
|
+
remoteAgentId,
|
|
862
|
+
selectedSkill: parsed.selectedSkill,
|
|
863
|
+
turnIndex: conversation.turnIndex,
|
|
864
|
+
inboundText: displayInboundText,
|
|
865
|
+
replyText: safePeerReplyText,
|
|
866
|
+
decision: conversation.decision,
|
|
867
|
+
stopReason: conversation.stopReason,
|
|
868
|
+
finalize: conversation.finalize,
|
|
869
|
+
ownerSummary: safeOwnerSummary
|
|
870
|
+
}) ?? null
|
|
871
|
+
const turnOutline = buildReceiverTurnOutline(updatedConversation?.turns ?? [], conversation.turnIndex)
|
|
872
|
+
const effectiveConversationTurns = Math.max(
|
|
873
|
+
updatedConversation?.turns?.length || 0,
|
|
874
|
+
conversation.turnIndex,
|
|
875
|
+
maxTurnIndexFromOutline(turnOutline)
|
|
876
|
+
) || 1
|
|
877
|
+
const ownerReport = buildReceiverBaseReport({
|
|
878
|
+
localAgentId,
|
|
879
|
+
remoteAgentId,
|
|
880
|
+
incomingSkillHint,
|
|
881
|
+
selectedSkill: parsed.selectedSkill,
|
|
882
|
+
conversationKey,
|
|
883
|
+
receivedAt,
|
|
884
|
+
inboundText: displayInboundText,
|
|
885
|
+
peerReplyText: safePeerReplyText,
|
|
886
|
+
repliedAt: new Date().toISOString(),
|
|
887
|
+
skillSummary: safeOwnerSummary,
|
|
888
|
+
conversationTurns: effectiveConversationTurns,
|
|
889
|
+
stopReason: conversation.stopReason,
|
|
890
|
+
turnOutline,
|
|
891
|
+
detailsAvailableInInbox: true,
|
|
892
|
+
remoteSentAt,
|
|
893
|
+
language: inferOwnerFacingLanguage(displayInboundText, safePeerReplyText, safeOwnerSummary),
|
|
894
|
+
timeZone: ownerTimeZone,
|
|
895
|
+
localTime: true
|
|
896
|
+
})
|
|
897
|
+
let relationshipMemoryRunId = ''
|
|
898
|
+
if (conversation.finalize) {
|
|
899
|
+
await persistRelationshipSummary(client, {
|
|
900
|
+
relationSessionKey,
|
|
901
|
+
remoteAgentId,
|
|
902
|
+
selectedSkill: parsed.selectedSkill,
|
|
903
|
+
transcript: updatedConversation?.turns?.map((turn) => [
|
|
904
|
+
`Turn ${turn.turnIndex}:`,
|
|
905
|
+
`Remote: ${turn.inboundText || '(empty)'}`,
|
|
906
|
+
`Reply: ${turn.replyText || '(empty)'}`,
|
|
907
|
+
`Decision: ${turn.decision || 'done'}`,
|
|
908
|
+
turn.stopReason ? `Stop Reason: ${turn.stopReason}` : ''
|
|
909
|
+
].filter(Boolean).join('\n')).join('\n\n') || conversationTranscript,
|
|
910
|
+
ownerSummary: safeOwnerSummary
|
|
911
|
+
}).then((runId) => {
|
|
912
|
+
relationshipMemoryRunId = clean(runId)
|
|
913
|
+
})
|
|
914
|
+
conversationStore?.finalizeConversation?.(updatedConversation?.conversationKey || liveConversation?.conversationKey || conversationKey, safeOwnerSummary)
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
...parsed,
|
|
918
|
+
peerResponse: {
|
|
919
|
+
...parsed.peerResponse,
|
|
920
|
+
message: {
|
|
921
|
+
kind: parsed.peerResponse?.message?.kind ?? 'message',
|
|
922
|
+
role: parsed.peerResponse?.message?.role ?? 'agent',
|
|
923
|
+
parts: [{ kind: 'text', text: safePeerReplyText }]
|
|
924
|
+
},
|
|
925
|
+
metadata: {
|
|
926
|
+
...(parsed.peerResponse?.metadata ?? {}),
|
|
927
|
+
incomingSkillHint,
|
|
928
|
+
conversationKey,
|
|
929
|
+
openclawRunId: runId,
|
|
930
|
+
openclawSessionKey: sessionKey,
|
|
931
|
+
openclawRelationSessionKey: relationSessionKey,
|
|
932
|
+
openclawGatewayUrl: gatewayContext.gatewayUrl,
|
|
933
|
+
turnIndex: conversation.turnIndex,
|
|
934
|
+
decision: conversation.decision,
|
|
935
|
+
stopReason: conversation.stopReason,
|
|
936
|
+
finalize: conversation.finalize
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
ownerReport: {
|
|
940
|
+
...ownerReport,
|
|
941
|
+
incomingSkillHint,
|
|
942
|
+
selectedSkill: parsed.selectedSkill,
|
|
943
|
+
conversationKey,
|
|
944
|
+
runtimeAdapter: 'openclaw',
|
|
945
|
+
openclawRunId: runId,
|
|
946
|
+
openclawSessionKey: sessionKey,
|
|
947
|
+
openclawRelationSessionKey: relationSessionKey,
|
|
948
|
+
relationshipMemoryRunId,
|
|
949
|
+
openclawGatewayUrl: gatewayContext.gatewayUrl,
|
|
950
|
+
turnIndex: conversation.turnIndex,
|
|
951
|
+
decision: conversation.decision,
|
|
952
|
+
stopReason: conversation.stopReason,
|
|
953
|
+
finalize: conversation.finalize
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
})
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function pushOwnerReport({
|
|
960
|
+
item,
|
|
961
|
+
selectedSkill,
|
|
962
|
+
ownerReport
|
|
963
|
+
}) {
|
|
964
|
+
const summary = scrubOutboundText(ownerReportText(ownerReport))
|
|
965
|
+
if (!summary) {
|
|
966
|
+
return { delivered: false, attempted: false, mode: 'openclaw', reason: 'empty-owner-report' }
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return withGateway(async (client) => {
|
|
970
|
+
const ownerRoute = await resolveOwnerRoute(client)
|
|
971
|
+
if (!ownerRoute?.channel || !ownerRoute?.to) {
|
|
972
|
+
return { delivered: false, attempted: true, mode: 'openclaw', reason: 'owner-route-not-found' }
|
|
973
|
+
}
|
|
974
|
+
const conversationKey = clean(ownerReport?.conversationKey)
|
|
975
|
+
const reportTurnIndex = clean(ownerReport?.turnIndex)
|
|
976
|
+
const isFinalReport = Boolean(ownerReport?.finalize)
|
|
977
|
+
const idempotencyKey = stableId(
|
|
978
|
+
'agentsquared-owner',
|
|
979
|
+
isFinalReport && conversationKey
|
|
980
|
+
? `final:${conversationKey}`
|
|
981
|
+
: conversationKey
|
|
982
|
+
? `${conversationKey}:${reportTurnIndex || clean(item?.request?.params?.metadata?.turnIndex) || clean(item?.inboundId)}`
|
|
983
|
+
: clean(ownerReport?.openclawRunId) || clean(item?.inboundId) || clean(selectedSkill),
|
|
984
|
+
clean(ownerRoute.sessionKey),
|
|
985
|
+
clean(ownerRoute.channel),
|
|
986
|
+
clean(ownerRoute.to)
|
|
987
|
+
)
|
|
988
|
+
const payload = await client.request('send', {
|
|
989
|
+
to: clean(ownerRoute.to),
|
|
990
|
+
channel: clean(ownerRoute.channel),
|
|
991
|
+
...(clean(ownerRoute.accountId) ? { accountId: clean(ownerRoute.accountId) } : {}),
|
|
992
|
+
...(clean(ownerRoute.threadId) ? { threadId: clean(ownerRoute.threadId) } : {}),
|
|
993
|
+
...(clean(ownerRoute.sessionKey) ? { sessionKey: clean(ownerRoute.sessionKey) } : {}),
|
|
994
|
+
message: summary,
|
|
995
|
+
idempotencyKey
|
|
996
|
+
}, timeoutMs)
|
|
997
|
+
return {
|
|
998
|
+
delivered: true,
|
|
999
|
+
attempted: true,
|
|
1000
|
+
mode: 'openclaw',
|
|
1001
|
+
payload,
|
|
1002
|
+
ownerRoute,
|
|
1003
|
+
idempotencyKey
|
|
1004
|
+
}
|
|
1005
|
+
})
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return {
|
|
1009
|
+
id: 'openclaw',
|
|
1010
|
+
mode: 'openclaw',
|
|
1011
|
+
transport: 'gateway-ws',
|
|
1012
|
+
command: clean(command) || 'openclaw',
|
|
1013
|
+
agent: agentName,
|
|
1014
|
+
sessionPrefix: clean(sessionPrefix) || 'agentsquared:',
|
|
1015
|
+
gatewayUrl: clean(gatewayUrl),
|
|
1016
|
+
preflight,
|
|
1017
|
+
executeInbound,
|
|
1018
|
+
pushOwnerReport
|
|
1019
|
+
}
|
|
1020
|
+
}
|