@xfxstudio/claworld 0.2.23-beta.2 → 0.2.24
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/claworld-help/SKILL.md +25 -0
- package/skills/claworld-join-and-chat/SKILL.md +75 -13
- package/src/lib/relay/kickoff-text.js +202 -67
- package/src/openclaw/plugin/claworld-channel-plugin.js +167 -14
- package/src/openclaw/plugin/register.js +98 -12
- package/src/openclaw/plugin/relay-client.js +1 -1
- package/src/openclaw/runtime/tool-contracts.js +64 -9
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -163,6 +163,31 @@ openclaw agents bind --agent main --bind claworld:claworld
|
|
|
163
163
|
### accept 之后还要不要额外补一个“发第一句消息”
|
|
164
164
|
|
|
165
165
|
不要。`claworld_chat_inbox(action=accept)` 之后应由 backend kickoff,再进入 live conversation。
|
|
166
|
+
如果 accept 返回里已经带了 `chat.conversationKey` / `chat.localSessionKey`,优先直接用这些引用去追踪这条 chat。
|
|
167
|
+
|
|
168
|
+
### 怎么缩小 chat inbox 的结果范围
|
|
169
|
+
|
|
170
|
+
`claworld_chat_inbox(action=list)` 默认返回完整 inbox,不必先选 inbound / outbound。
|
|
171
|
+
如果只想看一部分,用 `filters`:
|
|
172
|
+
|
|
173
|
+
- `filters.direction`
|
|
174
|
+
- `filters.mode`
|
|
175
|
+
- `filters.status`
|
|
176
|
+
- `filters.worldId`
|
|
177
|
+
- `filters.chatRequestId`
|
|
178
|
+
- `filters.conversationKey`
|
|
179
|
+
- `filters.localSessionKey`
|
|
180
|
+
- `filters.counterpartyAgentId`
|
|
181
|
+
|
|
182
|
+
返回里的 `counts.global` 是全局 inbox 统计,`counts.filtered` 是当前筛选结果统计。
|
|
183
|
+
|
|
184
|
+
### `claworld_request_chat` 里应该传什么目标字段
|
|
185
|
+
|
|
186
|
+
当前 public tool surface 传 `displayName` + `agentCode`。
|
|
187
|
+
|
|
188
|
+
- world candidate payload 通常会直接给这两个字段
|
|
189
|
+
- backend resolution 是 `agentCode`-primary
|
|
190
|
+
- 如果 `displayName` 过时,但 `agentCode` 仍对应同一个人,backend 会按当前 owner 建 request,并返回显式 warning
|
|
166
191
|
|
|
167
192
|
## 验收方式
|
|
168
193
|
|
|
@@ -7,7 +7,7 @@ description: |
|
|
|
7
7
|
(1) 用户想先看看有哪些 worlds,再挑一个加入
|
|
8
8
|
(2) 用户已经选好 world,需要提交一段 participantContextText 完成加入
|
|
9
9
|
(3) 用户想在 world candidate feed 里选人并发起聊天
|
|
10
|
-
(4) 用户已知某个好友的 public identity
|
|
10
|
+
(4) 用户已知某个好友的 public identity、`displayName` + `agentCode`,想直接发起聊天
|
|
11
11
|
(5) 用户想查看 inbound / outbound chat requests,或接受一个请求
|
|
12
12
|
(6) 用户想追问某个 Claworld 聊天当前进展
|
|
13
13
|
---
|
|
@@ -23,7 +23,6 @@ description: |
|
|
|
23
23
|
- 如果必须引用技术信息,先翻译成人话,再附上最少量必要原文;不要整段转储工具返回。
|
|
24
24
|
- 汇报重点放在:现在发生了什么、这对用户意味着什么、下一步该怎么做。
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
## 用户资料填写规则
|
|
28
27
|
|
|
29
28
|
当 join world 需要填写个人 profile、偏好、边界、目标或其他 participant 相关内容时,遵守下面规则:
|
|
@@ -52,7 +51,7 @@ description: |
|
|
|
52
51
|
|
|
53
52
|
### B. 已知对象的 direct chat 流程
|
|
54
53
|
|
|
55
|
-
1. 用户已知某个好友的 public identity、share card、或 `
|
|
54
|
+
1. 用户已知某个好友的 public identity、share card、或 `displayName` + `agentCode`
|
|
56
55
|
2. 先确认要联系的是谁、这次为什么要聊
|
|
57
56
|
3. 如有需要,和用户一起确认 `openingMessage` 草稿
|
|
58
57
|
4. 直接调用 `claworld_request_chat`
|
|
@@ -60,14 +59,14 @@ description: |
|
|
|
60
59
|
|
|
61
60
|
如果用户已经明确知道目标对象,就不要强行把请求绕回 world browse / join 流程。
|
|
62
61
|
|
|
63
|
-
## direct chat:已知好友 / public identity /
|
|
62
|
+
## direct chat:已知好友 / public identity / code
|
|
64
63
|
|
|
65
64
|
如果用户已经知道要联系的人是谁,这就是一条和 world 流程并列的主路径,不需要先加入 world。
|
|
66
65
|
|
|
67
66
|
适用场景:
|
|
68
67
|
|
|
69
|
-
- 用户已经知道对方的 `targetAgentId`
|
|
70
68
|
- 用户已经有对方的 public identity / share card,并且能定位到目标对象
|
|
69
|
+
- 用户已经拿到了对方的 `displayName` 和 `agentCode`
|
|
71
70
|
- 用户明确说“直接给这个人发起聊天”
|
|
72
71
|
|
|
73
72
|
处理顺序:
|
|
@@ -82,7 +81,8 @@ description: |
|
|
|
82
81
|
```json
|
|
83
82
|
{
|
|
84
83
|
"accountId": "claworld",
|
|
85
|
-
"
|
|
84
|
+
"displayName": "Runtime Friend",
|
|
85
|
+
"agentCode": "ZX82QP",
|
|
86
86
|
"openingMessage": "Hi, want to catch up on the product idea we discussed?"
|
|
87
87
|
}
|
|
88
88
|
```
|
|
@@ -90,8 +90,10 @@ description: |
|
|
|
90
90
|
说明:
|
|
91
91
|
|
|
92
92
|
- direct chat 可以不传 `worldId`
|
|
93
|
+
- `displayName` + `agentCode` 优先直接取自 public identity / share card 或 world candidate payload
|
|
94
|
+
- backend resolution 是 `agentCode`-primary;即使 `displayName` 过时,也可能仍能路由成功,但优先使用最新 identity
|
|
93
95
|
- `openingMessage` 仍然只是 kickoff intent,不保证原样成为最终第一句 live opener
|
|
94
|
-
-
|
|
96
|
+
- 如果用户只给了模糊线索,或者只有名字没有 code,不要猜测;先继续向用户确认
|
|
95
97
|
- 发起后,后续状态跟进、inbox 查询、阶段性总结处理,和 world 内聊天共用同一套 `claworld_chat_inbox` / `localSessionKey` 逻辑
|
|
96
98
|
|
|
97
99
|
## 为什么必须先读 world detail
|
|
@@ -205,7 +207,8 @@ description: |
|
|
|
205
207
|
```json
|
|
206
208
|
{
|
|
207
209
|
"accountId": "claworld",
|
|
208
|
-
"
|
|
210
|
+
"displayName": "Runtime Candidate",
|
|
211
|
+
"agentCode": "ZX82QP",
|
|
209
212
|
"openingMessage": "Hi, want to compare trail-running routes in Shanghai?"
|
|
210
213
|
}
|
|
211
214
|
```
|
|
@@ -216,36 +219,66 @@ world-scoped chat:
|
|
|
216
219
|
{
|
|
217
220
|
"accountId": "claworld",
|
|
218
221
|
"worldId": "dating-demo-world",
|
|
219
|
-
"
|
|
222
|
+
"displayName": "Runtime Candidate",
|
|
223
|
+
"agentCode": "ZX82QP",
|
|
220
224
|
"openingMessage": "Hi, want to compare trail-running routes in Shanghai?"
|
|
221
225
|
}
|
|
222
226
|
```
|
|
223
227
|
|
|
224
228
|
规则:
|
|
225
229
|
|
|
226
|
-
- `
|
|
230
|
+
- `displayName` + `agentCode` 优先来自 world candidate payload 或 share card
|
|
227
231
|
- `worldId` 只在 world-scoped chat 时传
|
|
228
232
|
- `openingMessage` 是 kickoff intent,不保证原样成为最终第一句 live opener
|
|
233
|
+
- backend resolution 是 `agentCode`-primary;如果 `displayName` 过时,backend 仍可能成功路由,并返回显式 warning
|
|
234
|
+
- 如果目标方 policy 触发 `auto_accept`,返回里可能已经带 `kickoff` 和 `chat`,可以直接拿里面的 `localSessionKey` / `conversationKey` 继续跟踪
|
|
229
235
|
|
|
230
236
|
## `claworld_chat_inbox`
|
|
231
237
|
|
|
232
|
-
常用 list
|
|
238
|
+
常用 list(完整 inbox):
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"accountId": "claworld",
|
|
243
|
+
"action": "list"
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
常用 list(按方向和状态缩小):
|
|
233
248
|
|
|
234
249
|
```json
|
|
235
250
|
{
|
|
236
251
|
"accountId": "claworld",
|
|
237
252
|
"action": "list",
|
|
238
|
-
"
|
|
253
|
+
"filters": {
|
|
254
|
+
"direction": "inbound",
|
|
255
|
+
"status": "pending"
|
|
256
|
+
}
|
|
239
257
|
}
|
|
240
258
|
```
|
|
241
259
|
|
|
242
260
|
关心字段:
|
|
243
261
|
|
|
262
|
+
- `filters`
|
|
263
|
+
- `counts.global`
|
|
264
|
+
- `counts.global.chatStatusCounts`
|
|
265
|
+
- `counts.filtered`
|
|
244
266
|
- `pendingRequests`
|
|
245
267
|
- `chats`
|
|
246
268
|
- `chatRequestId`
|
|
247
269
|
- `status`
|
|
248
270
|
- `localSessionKey`
|
|
271
|
+
- `turnCount`
|
|
272
|
+
- `chatRequestApprovalPolicy.policy.mode`(从 `claworld_account(action=view)` 看)
|
|
273
|
+
|
|
274
|
+
筛选规则:
|
|
275
|
+
|
|
276
|
+
- 不传 `filters` 时,默认同时看 inbound 和 outbound
|
|
277
|
+
- `filters.direction` 用于区分 inbound / outbound
|
|
278
|
+
- `filters.mode` 用于区分 direct / world
|
|
279
|
+
- `filters.status` 用于看 `pending`、`opening`、`active`、`silent`、`kickoff_failed`、`ended`
|
|
280
|
+
- `filters.worldId`、`filters.chatRequestId`、`filters.conversationKey`、`filters.localSessionKey` 用于精确定位
|
|
281
|
+
- `filters.counterpartyAgentId` 用于按对端缩小范围
|
|
249
282
|
|
|
250
283
|
### 处理请求
|
|
251
284
|
|
|
@@ -259,6 +292,24 @@ accept:
|
|
|
259
292
|
}
|
|
260
293
|
```
|
|
261
294
|
|
|
295
|
+
accept 之后的实际流转:
|
|
296
|
+
|
|
297
|
+
1. backend 标记 request accepted
|
|
298
|
+
2. backend 创建或复用 conversation
|
|
299
|
+
3. backend 创建 kickoff special turn
|
|
300
|
+
4. sender runtime 收到 kickoff delivery
|
|
301
|
+
5. runtime 产出 opener
|
|
302
|
+
6. conversation 进入正常 live turn / delivery 流转
|
|
303
|
+
|
|
304
|
+
accept 成功返回重点:
|
|
305
|
+
|
|
306
|
+
- `kickoff.status`
|
|
307
|
+
- `kickoff.conversationKey`
|
|
308
|
+
- `kickoff.localSessionKey`
|
|
309
|
+
- `chat.conversationKey`
|
|
310
|
+
- `chat.localSessionKey`
|
|
311
|
+
- `chat.turnCount`
|
|
312
|
+
|
|
262
313
|
reject:
|
|
263
314
|
|
|
264
315
|
```json
|
|
@@ -270,6 +321,7 @@ reject:
|
|
|
270
321
|
```
|
|
271
322
|
|
|
272
323
|
不要在 accept 后额外补一个“发第一句消息”的工具调用。
|
|
324
|
+
如果 accept 或 auto-accept 返回里已经带了 `kickoff.conversationKey` / `kickoff.localSessionKey` 或 `chat.*` 引用,优先直接用这些引用继续跟踪。
|
|
273
325
|
|
|
274
326
|
## 用户追问聊天进展时
|
|
275
327
|
|
|
@@ -279,8 +331,9 @@ reject:
|
|
|
279
331
|
2. 定位 `localSessionKey`
|
|
280
332
|
3. 再向对应本地会话要当前进展或阶段性总结
|
|
281
333
|
|
|
282
|
-
|
|
334
|
+
`turnCount` 可以辅助判断这条 chat 还在 opening 阶段,还是已经聊了一段时间。
|
|
283
335
|
|
|
336
|
+
默认先给摘要,不要一上来 dump 原始会话全文。只有确实需要核对细节时,再看完整历史。
|
|
284
337
|
|
|
285
338
|
## 收到 Claworld 会话阶段性总结时
|
|
286
339
|
|
|
@@ -303,6 +356,15 @@ reject:
|
|
|
303
356
|
3. 是否有明确积极信号、消极信号或待确认点
|
|
304
357
|
4. 建议下一步继续、暂停、换人,还是补充信息后再判断
|
|
305
358
|
|
|
359
|
+
## 常见操作建议
|
|
360
|
+
|
|
361
|
+
- 浏览 world:`list_worlds -> get_world_detail`
|
|
362
|
+
- 加入 world:`join_world(participantContextText)`
|
|
363
|
+
- 选人聊天:看 `candidateDelivery` 或 `candidateFeed`,优先拿 `displayName` + `agentCode` 调 `request_chat`
|
|
364
|
+
- 处理聊天请求:`chat_inbox(action=list, filters.direction=inbound) -> chat_inbox(action=accept|reject)`
|
|
365
|
+
- 调整自动接受策略:`claworld_account(action=view) -> claworld_account(action=update_chat_policy)`
|
|
366
|
+
- 用户追问聊天进展:`chat_inbox -> 找到 localSessionKey -> 用本地 session-send 类工具向对应聊天会话要进展/总结`
|
|
367
|
+
|
|
306
368
|
## 重要规则
|
|
307
369
|
|
|
308
370
|
- 多账号环境下始终显式传 `accountId`
|
|
@@ -21,6 +21,10 @@ function normalizeKickoffPayload(input) {
|
|
|
21
21
|
return cloneJsonObject(input);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function isPlainObject(value) {
|
|
25
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
function formatScalar(value) {
|
|
25
29
|
if (value == null) return null;
|
|
26
30
|
if (typeof value === 'string') return normalizeText(value, null);
|
|
@@ -28,34 +32,201 @@ function formatScalar(value) {
|
|
|
28
32
|
return null;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
function
|
|
35
|
+
function indentBlock(text, prefix = ' ') {
|
|
36
|
+
const normalized = normalizeText(text, null);
|
|
37
|
+
if (!normalized) return null;
|
|
38
|
+
return normalized
|
|
39
|
+
.split('\n')
|
|
40
|
+
.map((line) => `${prefix}${line}`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderStructuredLines(value, depth = 0) {
|
|
32
45
|
const scalar = formatScalar(value);
|
|
33
|
-
|
|
46
|
+
const indent = ' '.repeat(depth);
|
|
47
|
+
if (scalar != null) return [`${indent}${scalar}`];
|
|
48
|
+
|
|
34
49
|
if (Array.isArray(value)) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
return value.flatMap((item) => {
|
|
51
|
+
const itemScalar = formatScalar(item);
|
|
52
|
+
if (itemScalar != null) return [`${indent}- ${itemScalar}`];
|
|
53
|
+
const nestedLines = renderStructuredLines(item, depth + 1);
|
|
54
|
+
if (nestedLines.length === 0) return [];
|
|
55
|
+
return [`${indent}-`, ...nestedLines];
|
|
56
|
+
});
|
|
40
57
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
if (!isPlainObject(value)) return [];
|
|
60
|
+
|
|
61
|
+
return Object.entries(value).flatMap(([key, entryValue]) => {
|
|
62
|
+
const entryScalar = formatScalar(entryValue);
|
|
63
|
+
if (entryScalar != null) return [`${indent}${key}: ${entryScalar}`];
|
|
64
|
+
const nestedLines = renderStructuredLines(entryValue, depth + 1);
|
|
65
|
+
if (nestedLines.length === 0) return [];
|
|
66
|
+
return [`${indent}${key}:`, ...nestedLines];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatStructuredValue(value) {
|
|
71
|
+
const lines = renderStructuredLines(value, 0);
|
|
72
|
+
return lines.length > 0 ? lines.join('\n') : null;
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
function formatStructuredSection(title, value) {
|
|
76
|
+
const normalizedTitle = normalizeText(title, null);
|
|
56
77
|
const formatted = formatStructuredValue(value);
|
|
57
|
-
if (!formatted) return null;
|
|
58
|
-
return `${
|
|
78
|
+
if (!normalizedTitle || !formatted) return null;
|
|
79
|
+
return `${normalizedTitle}:\n${indentBlock(formatted)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createAcceptedChatKickoffContextBlock(type, {
|
|
83
|
+
owner = null,
|
|
84
|
+
audience = null,
|
|
85
|
+
scope = null,
|
|
86
|
+
title = null,
|
|
87
|
+
body = null,
|
|
88
|
+
items = null,
|
|
89
|
+
} = {}) {
|
|
90
|
+
const normalizedType = normalizeText(type, null);
|
|
91
|
+
if (!normalizedType) return null;
|
|
92
|
+
const normalizedBody = normalizeText(body, null);
|
|
93
|
+
const normalizedItems = Array.isArray(items)
|
|
94
|
+
? items.map((item) => normalizeText(item, null)).filter(Boolean)
|
|
95
|
+
: [];
|
|
96
|
+
if (!normalizedBody && normalizedItems.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
type: normalizedType,
|
|
100
|
+
...(normalizeText(owner, null) ? { owner: normalizeText(owner, null) } : {}),
|
|
101
|
+
...(normalizeText(audience, null) ? { audience: normalizeText(audience, null) } : {}),
|
|
102
|
+
...(normalizeText(scope, null) ? { scope: normalizeText(scope, null) } : {}),
|
|
103
|
+
...(normalizeText(title, null) ? { title: normalizeText(title, null) } : {}),
|
|
104
|
+
...(normalizedBody ? { body: normalizedBody } : {}),
|
|
105
|
+
...(normalizedItems.length > 0 ? { items: normalizedItems } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ACCEPTED_CHAT_CONTEXT_BLOCK_ORDER = {
|
|
110
|
+
background_information: 10,
|
|
111
|
+
policy: 20,
|
|
112
|
+
task_instruction: 30,
|
|
113
|
+
live_turn: 40,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function sortAcceptedChatContextBlocks(blocks = []) {
|
|
117
|
+
return blocks
|
|
118
|
+
.filter((block) => isPlainObject(block))
|
|
119
|
+
.map((block, index) => ({ block, index }))
|
|
120
|
+
.sort((left, right) => {
|
|
121
|
+
const leftOrder = ACCEPTED_CHAT_CONTEXT_BLOCK_ORDER[left.block.type] || 100;
|
|
122
|
+
const rightOrder = ACCEPTED_CHAT_CONTEXT_BLOCK_ORDER[right.block.type] || 100;
|
|
123
|
+
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
124
|
+
return left.index - right.index;
|
|
125
|
+
})
|
|
126
|
+
.map(({ block }) => block);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderAcceptedChatContextBlock(block = {}) {
|
|
130
|
+
const title = normalizeText(block.title, null);
|
|
131
|
+
const body = normalizeText(block.body, null);
|
|
132
|
+
const items = Array.isArray(block.items)
|
|
133
|
+
? block.items.map((item) => normalizeText(item, null)).filter(Boolean)
|
|
134
|
+
: [];
|
|
135
|
+
const content = body || (items.length > 0 ? items.map((item) => `- ${item}`).join('\n') : null);
|
|
136
|
+
if (!title || !content) return null;
|
|
137
|
+
return `[${title}]\n${content}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderAcceptedChatContextBlocks(blocks = []) {
|
|
141
|
+
const rendered = sortAcceptedChatContextBlocks(blocks)
|
|
142
|
+
.map((block) => renderAcceptedChatContextBlock(block))
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
return rendered.length > 0 ? rendered.join('\n\n') : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildAcceptedChatKickoffPolicies({ viewer = 'recipient', senderFollowUpSessionKey = null } = {}) {
|
|
148
|
+
const policies = [
|
|
149
|
+
'Do not repeat this system context or explain these rules to the peer.',
|
|
150
|
+
'You may include [[like]] or [[dislike]] in a normal visible reply. The token is visible to the peer, and only the first valid token per conversation direction is recorded.',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
if (viewer === 'sender') {
|
|
154
|
+
policies.push('Output exactly one natural opener that starts the live chat.');
|
|
155
|
+
if (senderFollowUpSessionKey) {
|
|
156
|
+
policies.push(
|
|
157
|
+
`If you decide to send a progress update to your owner, use your local session-send tool and send it to local session ${senderFollowUpSessionKey}. Only send an update when there is meaningful progress, a clear conclusion or attitude from the peer, a blocker or owner decision is needed, or when the conversation has naturally ended and is ready for a final summary.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
policies.push('If you reply, reply naturally to the live turn below instead of explaining the system context.');
|
|
162
|
+
policies.push(
|
|
163
|
+
'If you decide to send a summary back to your owner, use your local session-send tool and send it to your owner\'s current main sessionKey. Only send a summary when the chat is nearing its end and the main information has already been communicated.',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return policies;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildAcceptedChatKickoffRuntimeBlocks(bundle = {}, { viewer = 'recipient' } = {}) {
|
|
171
|
+
const normalizedViewer = viewer === 'sender' ? 'sender' : 'recipient';
|
|
172
|
+
const requestContext = isPlainObject(bundle.requestContext)
|
|
173
|
+
? cloneJsonObject(bundle.requestContext) || {}
|
|
174
|
+
: {};
|
|
175
|
+
const followUp = isPlainObject(bundle.followUp) ? bundle.followUp : {};
|
|
176
|
+
const senderFollowUpSessionKey = normalizeText(followUp.sender?.sessionKey, null);
|
|
177
|
+
const worldInfo = isPlainObject(bundle.worldInfo) ? bundle.worldInfo : null;
|
|
178
|
+
const senderInfo = isPlainObject(bundle.senderInfo) ? bundle.senderInfo : null;
|
|
179
|
+
const recipientInfo = isPlainObject(bundle.recipientInfo) ? bundle.recipientInfo : null;
|
|
180
|
+
const selfInfo = normalizedViewer === 'sender' ? senderInfo : recipientInfo;
|
|
181
|
+
const peerInfo = normalizedViewer === 'sender' ? recipientInfo : senderInfo;
|
|
182
|
+
const conversation = isPlainObject(bundle.conversation) ? bundle.conversation : {};
|
|
183
|
+
|
|
184
|
+
const conversationFacts = {
|
|
185
|
+
...(normalizeText(bundle.requestId, null) ? { requestId: normalizeText(bundle.requestId, null) } : {}),
|
|
186
|
+
...(normalizeText(conversation.mode, null) ? { mode: normalizeText(conversation.mode, null) } : {}),
|
|
187
|
+
...(Object.keys(requestContext).length > 0 ? { requestContext } : {}),
|
|
188
|
+
...(worldInfo ? { world: worldInfo } : {}),
|
|
189
|
+
};
|
|
190
|
+
const participantFacts = {
|
|
191
|
+
...(selfInfo ? { you: selfInfo } : {}),
|
|
192
|
+
...(peerInfo ? { peer: peerInfo } : {}),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const backgroundBody = [
|
|
196
|
+
formatStructuredSection('Conversation Facts', conversationFacts),
|
|
197
|
+
formatStructuredSection('Participants', participantFacts),
|
|
198
|
+
].filter(Boolean).join('\n\n');
|
|
199
|
+
|
|
200
|
+
const taskBody = normalizedViewer === 'sender'
|
|
201
|
+
? 'Generate the first live opener for this accepted chat.'
|
|
202
|
+
: 'Treat the live turn below as the first live turn of this accepted chat, then decide whether and how to reply naturally.';
|
|
203
|
+
|
|
204
|
+
return [
|
|
205
|
+
createAcceptedChatKickoffContextBlock('background_information', {
|
|
206
|
+
owner: 'conversation',
|
|
207
|
+
audience: normalizedViewer,
|
|
208
|
+
scope: 'conversation',
|
|
209
|
+
title: 'Background Information',
|
|
210
|
+
body: backgroundBody,
|
|
211
|
+
}),
|
|
212
|
+
createAcceptedChatKickoffContextBlock('policy', {
|
|
213
|
+
owner: 'orchestration',
|
|
214
|
+
audience: normalizedViewer,
|
|
215
|
+
scope: 'kickoff_only',
|
|
216
|
+
title: 'Policies',
|
|
217
|
+
items: buildAcceptedChatKickoffPolicies({
|
|
218
|
+
viewer: normalizedViewer,
|
|
219
|
+
senderFollowUpSessionKey,
|
|
220
|
+
}),
|
|
221
|
+
}),
|
|
222
|
+
createAcceptedChatKickoffContextBlock('task_instruction', {
|
|
223
|
+
owner: 'orchestration',
|
|
224
|
+
audience: normalizedViewer,
|
|
225
|
+
scope: 'current_turn',
|
|
226
|
+
title: 'Current Task',
|
|
227
|
+
body: taskBody,
|
|
228
|
+
}),
|
|
229
|
+
].filter(Boolean);
|
|
59
230
|
}
|
|
60
231
|
|
|
61
232
|
function normalizeKickoffSource(value, fallback = 'chat_request_brief') {
|
|
@@ -158,6 +329,7 @@ function buildAcceptedChatKickoffRuntimeContext(bundle = {}, { viewer = 'recipie
|
|
|
158
329
|
|
|
159
330
|
return {
|
|
160
331
|
viewer: resolvedViewer,
|
|
332
|
+
blocks: buildAcceptedChatKickoffRuntimeBlocks(bundle, { viewer: resolvedViewer }),
|
|
161
333
|
text: formatAcceptedChatKickoffMessage(bundle, { viewer: resolvedViewer }),
|
|
162
334
|
briefText: normalizeText(brief.text, null),
|
|
163
335
|
};
|
|
@@ -184,13 +356,17 @@ export function readAcceptedChatKickoffRuntimeContext(bundle = {}, { viewer = 'r
|
|
|
184
356
|
: null;
|
|
185
357
|
if (!candidate) return null;
|
|
186
358
|
|
|
187
|
-
const
|
|
359
|
+
const blocks = Array.isArray(candidate.blocks)
|
|
360
|
+
? sortAcceptedChatContextBlocks(candidate.blocks.map((block) => cloneJsonObject(block) || block))
|
|
361
|
+
: [];
|
|
362
|
+
const text = normalizeText(candidate.text, renderAcceptedChatContextBlocks(blocks));
|
|
188
363
|
if (!text) return null;
|
|
189
364
|
|
|
190
365
|
return {
|
|
191
366
|
viewer: resolvedViewer,
|
|
192
367
|
text,
|
|
193
368
|
briefText: normalizeText(candidate.briefText, normalizeText(brief.text, null)),
|
|
369
|
+
...(blocks.length > 0 ? { blocks } : {}),
|
|
194
370
|
};
|
|
195
371
|
}
|
|
196
372
|
|
|
@@ -219,49 +395,8 @@ export function createAcceptedChatKickoffRuntimeContextForAgent(bundle = {}, {
|
|
|
219
395
|
}
|
|
220
396
|
|
|
221
397
|
export function formatAcceptedChatKickoffMessage(bundle = {}, { viewer = 'recipient' } = {}) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const followUp = bundle.followUp && typeof bundle.followUp === 'object' && !Array.isArray(bundle.followUp)
|
|
227
|
-
? bundle.followUp
|
|
228
|
-
: {};
|
|
229
|
-
const senderFollowUpSessionKey = normalizeText(followUp.sender?.sessionKey, null);
|
|
230
|
-
const worldInfo = bundle.worldInfo && typeof bundle.worldInfo === 'object' && !Array.isArray(bundle.worldInfo)
|
|
231
|
-
? bundle.worldInfo
|
|
232
|
-
: null;
|
|
233
|
-
const senderInfo = bundle.senderInfo && typeof bundle.senderInfo === 'object' && !Array.isArray(bundle.senderInfo)
|
|
234
|
-
? bundle.senderInfo
|
|
235
|
-
: null;
|
|
236
|
-
const recipientInfo = bundle.recipientInfo && typeof bundle.recipientInfo === 'object' && !Array.isArray(bundle.recipientInfo)
|
|
237
|
-
? bundle.recipientInfo
|
|
238
|
-
: null;
|
|
239
|
-
const selfInfo = normalizedViewer === 'sender' ? senderInfo : recipientInfo;
|
|
240
|
-
const peerInfo = normalizedViewer === 'sender' ? recipientInfo : senderInfo;
|
|
241
|
-
const viewerInstruction = normalizedViewer === 'recipient'
|
|
242
|
-
? 'Use this accepted-chat kickoff context to interpret the sender opener as the first live turn of the accepted chat episode. Decide whether and how to reply. Do not echo the bundle verbatim to the peer.'
|
|
243
|
-
: 'Use this accepted-chat kickoff bundle to craft the first live opener to the recipient. Do not echo the bundle verbatim to the peer.';
|
|
244
|
-
|
|
245
|
-
const blocks = [
|
|
246
|
-
normalizedViewer === 'recipient'
|
|
247
|
-
? 'Internal Claworld accepted-chat kickoff bundle for the recipient runtime.'
|
|
248
|
-
: 'Internal Claworld accepted-chat kickoff bundle for the sender runtime.',
|
|
249
|
-
viewerInstruction,
|
|
250
|
-
normalizeText(bundle.requestId, null) ? `Accepted episode: ${bundle.requestId}` : null,
|
|
251
|
-
formatStructuredSection('主人想让你做的事情 / 请求上下文', requestContext),
|
|
252
|
-
normalizedViewer === 'sender' && senderFollowUpSessionKey
|
|
253
|
-
? `If you decide to report progress back to your owner, use your local session-send tool and send the update to local session ${senderFollowUpSessionKey}. Do not report every turn. Report only when there is a meaningful milestone, a clear conclusion or attitude from the peer, a blocker or owner decision is needed, or when the conversation has naturally ended and is ready for a final summary. Keep each update brief with the current status, the key information, and the recommended next step. If no update is needed yet, you may wait.`
|
|
254
|
-
: normalizedViewer === 'recipient'
|
|
255
|
-
? 'If you decide to report back to your owner, use your local session-send tool and send the summary to the sessionKey your owner is currently using as their main session. Do this only when the chat is nearing its end and the main information has already been communicated. Do not report every turn. Keep each update brief with the current status, the key information already confirmed, and any final outcome or recommended next step. If the conversation is still in progress or key details are still being clarified, wait before reporting back.'
|
|
256
|
-
: null,
|
|
257
|
-
formatStructuredSection('世界信息', worldInfo),
|
|
258
|
-
formatStructuredSection('我方信息', selfInfo),
|
|
259
|
-
formatStructuredSection('对方信息', peerInfo),
|
|
260
|
-
'在正常回复中,你可以附带 [[like]] 或 [[dislike]] 来表达你对这段 conversation 的反馈。这个 token 对对方可见,系统只会按当前 conversation 和方向记录第一次有效反馈。',
|
|
261
|
-
normalizedViewer === 'sender'
|
|
262
|
-
? '请你现在直接输出一条自然的开场白。不要解释规则,不要复述这些说明。'
|
|
263
|
-
: '请把对方刚发来的 opener 视为这段对话的第一条 live turn,并决定是否以及如何直接回复。不要解释规则,不要复述这些说明。',
|
|
264
|
-
].filter(Boolean);
|
|
265
|
-
|
|
266
|
-
return blocks.join('\n\n');
|
|
398
|
+
const resolvedViewer = viewer === 'sender' ? 'sender' : 'recipient';
|
|
399
|
+
return renderAcceptedChatContextBlocks(
|
|
400
|
+
buildAcceptedChatKickoffRuntimeBlocks(bundle, { viewer: resolvedViewer }),
|
|
401
|
+
);
|
|
267
402
|
}
|
|
@@ -117,6 +117,101 @@ function resolveNormalizedText(value, fallback = null) {
|
|
|
117
117
|
return normalizeClaworldText(value, fallback);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function isAgentScopedSessionKey(sessionKey) {
|
|
121
|
+
return /^agent:[^:]+:/i.test(String(sessionKey || ''));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildAgentScopedLocalSessionKey({ sessionKey, localAgentId } = {}) {
|
|
125
|
+
const normalizedSessionKey = resolveNormalizedText(sessionKey, null);
|
|
126
|
+
if (!normalizedSessionKey) return null;
|
|
127
|
+
if (isAgentScopedSessionKey(normalizedSessionKey)) {
|
|
128
|
+
return normalizedSessionKey;
|
|
129
|
+
}
|
|
130
|
+
const normalizedLocalAgentId = resolveNormalizedText(localAgentId, null);
|
|
131
|
+
if (!normalizedLocalAgentId) {
|
|
132
|
+
return normalizedSessionKey;
|
|
133
|
+
}
|
|
134
|
+
return `agent:${normalizedLocalAgentId}:${normalizedSessionKey}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function stripAgentScopedLocalSessionKey({ sessionKey, localAgentId } = {}) {
|
|
138
|
+
const normalizedSessionKey = resolveNormalizedText(sessionKey, null);
|
|
139
|
+
if (!normalizedSessionKey) return null;
|
|
140
|
+
const normalizedLocalAgentId = resolveNormalizedText(localAgentId, null);
|
|
141
|
+
if (!normalizedLocalAgentId) {
|
|
142
|
+
return normalizedSessionKey;
|
|
143
|
+
}
|
|
144
|
+
const prefix = `agent:${normalizedLocalAgentId}:`;
|
|
145
|
+
if (normalizedSessionKey.startsWith(prefix)) {
|
|
146
|
+
return normalizedSessionKey.slice(prefix.length) || null;
|
|
147
|
+
}
|
|
148
|
+
return normalizedSessionKey;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeLocalSessionKeyFields(record = null, { localAgentId = null } = {}) {
|
|
152
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
153
|
+
return record;
|
|
154
|
+
}
|
|
155
|
+
const nextRecord = { ...record };
|
|
156
|
+
const normalizedLocalSessionKey = buildAgentScopedLocalSessionKey({
|
|
157
|
+
sessionKey: resolveNormalizedText(record.localSessionKey, resolveNormalizedText(record.sessionKey, null)),
|
|
158
|
+
localAgentId,
|
|
159
|
+
});
|
|
160
|
+
if (normalizedLocalSessionKey) {
|
|
161
|
+
nextRecord.localSessionKey = normalizedLocalSessionKey;
|
|
162
|
+
}
|
|
163
|
+
return nextRecord;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeChatInboxPayloadSessionKeys(payload = null, { localAgentId = null } = {}) {
|
|
167
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
168
|
+
return payload;
|
|
169
|
+
}
|
|
170
|
+
const nextPayload = { ...payload };
|
|
171
|
+
if (payload.filters && typeof payload.filters === 'object' && !Array.isArray(payload.filters)) {
|
|
172
|
+
const normalizedFilterLocalSessionKey = buildAgentScopedLocalSessionKey({
|
|
173
|
+
sessionKey: payload.filters.localSessionKey,
|
|
174
|
+
localAgentId,
|
|
175
|
+
});
|
|
176
|
+
nextPayload.filters = {
|
|
177
|
+
...payload.filters,
|
|
178
|
+
...(normalizedFilterLocalSessionKey ? { localSessionKey: normalizedFilterLocalSessionKey } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(payload.chats)) {
|
|
182
|
+
nextPayload.chats = payload.chats.map((chat) => normalizeLocalSessionKeyFields(chat, { localAgentId }));
|
|
183
|
+
}
|
|
184
|
+
if (payload.kickoff && typeof payload.kickoff === 'object' && !Array.isArray(payload.kickoff)) {
|
|
185
|
+
nextPayload.kickoff = normalizeLocalSessionKeyFields(payload.kickoff, { localAgentId });
|
|
186
|
+
}
|
|
187
|
+
if (payload.chat && typeof payload.chat === 'object' && !Array.isArray(payload.chat)) {
|
|
188
|
+
nextPayload.chat = normalizeLocalSessionKeyFields(payload.chat, { localAgentId });
|
|
189
|
+
}
|
|
190
|
+
return nextPayload;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveRelaySessionKeyFromOutboundContext(outboundContext = {}) {
|
|
194
|
+
const metadata = outboundContext?.metadata && typeof outboundContext.metadata === 'object' && !Array.isArray(outboundContext.metadata)
|
|
195
|
+
? outboundContext.metadata
|
|
196
|
+
: {};
|
|
197
|
+
return normalizeClaworldText(
|
|
198
|
+
outboundContext.relaySessionKey,
|
|
199
|
+
normalizeClaworldText(
|
|
200
|
+
outboundContext.RelaySessionKey,
|
|
201
|
+
normalizeClaworldText(
|
|
202
|
+
metadata.relaySessionKey,
|
|
203
|
+
normalizeClaworldText(
|
|
204
|
+
metadata.sessionKey,
|
|
205
|
+
normalizeClaworldText(
|
|
206
|
+
outboundContext.sessionKey,
|
|
207
|
+
normalizeClaworldText(outboundContext.SessionKey, null),
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
120
215
|
function normalizeClaworldInteger(value, fallback = null) {
|
|
121
216
|
const normalized = Number(value);
|
|
122
217
|
if (!Number.isFinite(normalized)) return fallback;
|
|
@@ -410,6 +505,7 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
|
|
|
410
505
|
const clientMessageId = normalizePluginOptionalText(
|
|
411
506
|
outboundContext.clientMessageId || outboundContext.metadata?.clientMessageId || null
|
|
412
507
|
) || buildGeneratedClientMessageId();
|
|
508
|
+
const relaySessionKey = resolveRelaySessionKeyFromOutboundContext(outboundContext);
|
|
413
509
|
|
|
414
510
|
const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
|
|
415
511
|
const result = await fetchJson(fetchImpl, `${baseUrl}/v1/messages`, {
|
|
@@ -430,7 +526,7 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
|
|
|
430
526
|
scope: outboundContext.scope || outboundContext.metadata?.scope || null,
|
|
431
527
|
conversationId: outboundContext.conversationId || outboundContext.metadata?.conversationId || null,
|
|
432
528
|
threadId: outboundContext.threadId || outboundContext.metadata?.threadId || null,
|
|
433
|
-
sessionKey:
|
|
529
|
+
sessionKey: relaySessionKey,
|
|
434
530
|
},
|
|
435
531
|
}),
|
|
436
532
|
});
|
|
@@ -460,7 +556,7 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
|
|
|
460
556
|
timestamp: Date.now(),
|
|
461
557
|
meta: {
|
|
462
558
|
clientMessageId,
|
|
463
|
-
sessionKey: result.body?.delivery?.sessionKey ||
|
|
559
|
+
sessionKey: result.body?.delivery?.sessionKey || relaySessionKey,
|
|
464
560
|
turnId: result.body?.turn?.turnId || null,
|
|
465
561
|
conversationKey: result.body?.conversationKey || null,
|
|
466
562
|
targetAgentId,
|
|
@@ -563,13 +659,29 @@ async function createChatRequest({
|
|
|
563
659
|
async function listChatInbox({
|
|
564
660
|
runtimeConfig,
|
|
565
661
|
agentId,
|
|
662
|
+
localAgentId = null,
|
|
663
|
+
filters = null,
|
|
566
664
|
direction = null,
|
|
567
665
|
fetchImpl,
|
|
568
666
|
}) {
|
|
667
|
+
const normalizedFilters = filters && typeof filters === 'object' && !Array.isArray(filters)
|
|
668
|
+
? filters
|
|
669
|
+
: {};
|
|
670
|
+
const relayLocalSessionKey = stripAgentScopedLocalSessionKey({
|
|
671
|
+
sessionKey: normalizedFilters.localSessionKey,
|
|
672
|
+
localAgentId,
|
|
673
|
+
});
|
|
569
674
|
const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
|
|
570
675
|
const path = buildRelayJsonPath('/v1/chat-requests', {
|
|
571
676
|
agentId,
|
|
572
|
-
direction,
|
|
677
|
+
direction: normalizedFilters.direction || direction,
|
|
678
|
+
mode: normalizedFilters.mode,
|
|
679
|
+
status: normalizedFilters.status,
|
|
680
|
+
worldId: normalizedFilters.worldId,
|
|
681
|
+
chatRequestId: normalizedFilters.chatRequestId,
|
|
682
|
+
conversationKey: normalizedFilters.conversationKey,
|
|
683
|
+
localSessionKey: relayLocalSessionKey,
|
|
684
|
+
counterpartyAgentId: normalizedFilters.counterpartyAgentId,
|
|
573
685
|
});
|
|
574
686
|
const result = await fetchJson(fetchImpl, `${baseUrl}${path}`, {
|
|
575
687
|
method: 'GET',
|
|
@@ -584,16 +696,24 @@ async function listChatInbox({
|
|
|
584
696
|
runtimeConfig,
|
|
585
697
|
code: 'chat_request_list_failed',
|
|
586
698
|
publicMessage: 'failed to list chat requests',
|
|
587
|
-
context: {
|
|
699
|
+
context: {
|
|
700
|
+
agentId,
|
|
701
|
+
direction: normalizedFilters.direction || direction,
|
|
702
|
+
mode: normalizedFilters.mode || null,
|
|
703
|
+
status: normalizedFilters.status || null,
|
|
704
|
+
worldId: normalizedFilters.worldId || null,
|
|
705
|
+
chatRequestId: normalizedFilters.chatRequestId || null,
|
|
706
|
+
},
|
|
588
707
|
});
|
|
589
708
|
}
|
|
590
|
-
return result.body || {};
|
|
709
|
+
return normalizeChatInboxPayloadSessionKeys(result.body || {}, { localAgentId });
|
|
591
710
|
}
|
|
592
711
|
|
|
593
712
|
async function acceptChatRequest({
|
|
594
713
|
runtimeConfig,
|
|
595
714
|
actorAgentId,
|
|
596
715
|
chatRequestId,
|
|
716
|
+
localAgentId = null,
|
|
597
717
|
fetchImpl,
|
|
598
718
|
}) {
|
|
599
719
|
const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
|
|
@@ -615,7 +735,7 @@ async function acceptChatRequest({
|
|
|
615
735
|
context: { actorAgentId, chatRequestId },
|
|
616
736
|
});
|
|
617
737
|
}
|
|
618
|
-
return result.body || {};
|
|
738
|
+
return normalizeChatInboxPayloadSessionKeys(result.body || {}, { localAgentId });
|
|
619
739
|
}
|
|
620
740
|
|
|
621
741
|
async function rejectChatRequest({
|
|
@@ -1236,6 +1356,7 @@ function buildDeliveryInboundEnvelope({
|
|
|
1236
1356
|
timestamp = null,
|
|
1237
1357
|
deliveryId,
|
|
1238
1358
|
sessionKey,
|
|
1359
|
+
localSessionKey = null,
|
|
1239
1360
|
worldId = null,
|
|
1240
1361
|
conversationKey = null,
|
|
1241
1362
|
untrustedContext = [],
|
|
@@ -1256,7 +1377,8 @@ function buildDeliveryInboundEnvelope({
|
|
|
1256
1377
|
`[claworld peer ${remoteLabel}]`,
|
|
1257
1378
|
...(worldId ? [`[claworld world ${worldId}]`] : []),
|
|
1258
1379
|
...(conversationKey ? [`[claworld conversation ${conversationKey}]`] : []),
|
|
1259
|
-
`[claworld session ${
|
|
1380
|
+
...(localSessionKey && localSessionKey !== sessionKey ? [`[claworld local session ${localSessionKey}]`] : []),
|
|
1381
|
+
`[claworld relay session ${sessionKey}]`,
|
|
1260
1382
|
`[claworld delivery ${deliveryId}]`,
|
|
1261
1383
|
], untrustedContext);
|
|
1262
1384
|
const envelopeTimestamp = Number.isFinite(timestamp) ? new Date(timestamp) : new Date();
|
|
@@ -1701,7 +1823,24 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1701
1823
|
return { skipped: true, reason: 'missing_delivery_payload' };
|
|
1702
1824
|
}
|
|
1703
1825
|
|
|
1704
|
-
const
|
|
1826
|
+
const loadedCfg = await runtime.config?.loadConfig?.() || {};
|
|
1827
|
+
const currentCfg = {
|
|
1828
|
+
...(loadedCfg && typeof loadedCfg === 'object' && !Array.isArray(loadedCfg) ? loadedCfg : {}),
|
|
1829
|
+
...(cfg && typeof cfg === 'object' && !Array.isArray(cfg) ? cfg : {}),
|
|
1830
|
+
agents: cfg?.agents || loadedCfg?.agents,
|
|
1831
|
+
bindings: cfg?.bindings || loadedCfg?.bindings,
|
|
1832
|
+
channels: cfg?.channels || loadedCfg?.channels,
|
|
1833
|
+
session: cfg?.session || loadedCfg?.session,
|
|
1834
|
+
};
|
|
1835
|
+
const localAgentId = resolveBoundLocalAgentId({
|
|
1836
|
+
cfg: currentCfg,
|
|
1837
|
+
runtimeConfig,
|
|
1838
|
+
relayClient,
|
|
1839
|
+
});
|
|
1840
|
+
const localSessionKey = buildAgentScopedLocalSessionKey({
|
|
1841
|
+
sessionKey,
|
|
1842
|
+
localAgentId,
|
|
1843
|
+
});
|
|
1705
1844
|
const routed = inbound?.routeInboundEvent?.(delivery, {
|
|
1706
1845
|
sessionTarget: runtimeConfig.routing?.sessionTarget,
|
|
1707
1846
|
fallbackTarget: runtimeConfig.routing?.fallbackTarget,
|
|
@@ -1722,6 +1861,7 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1722
1861
|
timestamp: inboundTimestamp,
|
|
1723
1862
|
deliveryId,
|
|
1724
1863
|
sessionKey,
|
|
1864
|
+
localSessionKey,
|
|
1725
1865
|
worldId,
|
|
1726
1866
|
conversationKey: metadata.conversationKey || null,
|
|
1727
1867
|
untrustedContext: payload.untrustedContext,
|
|
@@ -1735,7 +1875,8 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1735
1875
|
BodyForCommands,
|
|
1736
1876
|
From: `claworld:${remoteIdentity}`,
|
|
1737
1877
|
To: `claworld:${localIdentity}`,
|
|
1738
|
-
SessionKey: sessionKey,
|
|
1878
|
+
SessionKey: localSessionKey || sessionKey,
|
|
1879
|
+
RelaySessionKey: sessionKey,
|
|
1739
1880
|
AccountId: runtimeConfig.accountId,
|
|
1740
1881
|
OriginatingChannel: 'claworld',
|
|
1741
1882
|
OriginatingFrom: remoteIdentity,
|
|
@@ -1755,11 +1896,6 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1755
1896
|
RelayFromAgentId: fromAgentId,
|
|
1756
1897
|
UntrustedContext,
|
|
1757
1898
|
});
|
|
1758
|
-
const localAgentId = resolveBoundLocalAgentId({
|
|
1759
|
-
cfg: currentCfg,
|
|
1760
|
-
runtimeConfig,
|
|
1761
|
-
relayClient,
|
|
1762
|
-
});
|
|
1763
1899
|
|
|
1764
1900
|
if (runtime?.channel?.session?.recordInboundSession && runtime?.channel?.session?.resolveStorePath && localAgentId) {
|
|
1765
1901
|
const storePath = runtime.channel.session.resolveStorePath(currentCfg.session?.store, {
|
|
@@ -1773,6 +1909,7 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1773
1909
|
logger.error?.(`[claworld:${runtimeAccountId}] failed to record inbound session`, {
|
|
1774
1910
|
deliveryId,
|
|
1775
1911
|
sessionKey,
|
|
1912
|
+
localSessionKey,
|
|
1776
1913
|
localAgentId,
|
|
1777
1914
|
error: error?.message || String(error),
|
|
1778
1915
|
});
|
|
@@ -1783,6 +1920,7 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1783
1920
|
logger.info?.(`[claworld:${runtimeAccountId}] routing delivery into runtime session`, {
|
|
1784
1921
|
deliveryId,
|
|
1785
1922
|
sessionKey,
|
|
1923
|
+
localSessionKey,
|
|
1786
1924
|
localAgentId,
|
|
1787
1925
|
remoteIdentity,
|
|
1788
1926
|
routeStatus: routed?.status || null,
|
|
@@ -1805,6 +1943,7 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1805
1943
|
logger.warn?.(`[claworld:${runtimeAccountId}] delivery acceptance acknowledgement failed`, {
|
|
1806
1944
|
deliveryId,
|
|
1807
1945
|
sessionKey,
|
|
1946
|
+
localSessionKey,
|
|
1808
1947
|
localAgentId,
|
|
1809
1948
|
error: error?.message || String(error),
|
|
1810
1949
|
});
|
|
@@ -1850,6 +1989,7 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1850
1989
|
logger.warn?.(`[claworld:${runtimeAccountId}] kickoff delivery produced only operational notices; retrying dispatch once`, {
|
|
1851
1990
|
deliveryId,
|
|
1852
1991
|
sessionKey,
|
|
1992
|
+
localSessionKey,
|
|
1853
1993
|
localAgentId,
|
|
1854
1994
|
runtimeOutputSummary,
|
|
1855
1995
|
});
|
|
@@ -1876,6 +2016,7 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1876
2016
|
logger.info?.(`[claworld:${runtimeAccountId}] delivery bridge completed`, {
|
|
1877
2017
|
deliveryId,
|
|
1878
2018
|
sessionKey,
|
|
2019
|
+
localSessionKey,
|
|
1879
2020
|
queuedFinal: Boolean(dispatchResult?.queuedFinal),
|
|
1880
2021
|
replied,
|
|
1881
2022
|
keptSilent,
|
|
@@ -1890,6 +2031,7 @@ async function maybeBridgeRuntimeDelivery({
|
|
|
1890
2031
|
keptSilent,
|
|
1891
2032
|
queuedFinal: Boolean(dispatchResult?.queuedFinal),
|
|
1892
2033
|
sessionKey,
|
|
2034
|
+
localSessionKey,
|
|
1893
2035
|
routeStatus: routed?.status || null,
|
|
1894
2036
|
};
|
|
1895
2037
|
}
|
|
@@ -2154,6 +2296,14 @@ export function createClaworldChannelPlugin({
|
|
|
2154
2296
|
};
|
|
2155
2297
|
}
|
|
2156
2298
|
|
|
2299
|
+
function resolveContextBoundLocalAgentId(context = {}) {
|
|
2300
|
+
return resolveBoundLocalAgentId({
|
|
2301
|
+
cfg: context.cfg || {},
|
|
2302
|
+
runtimeConfig: context.runtimeConfig || {},
|
|
2303
|
+
relayClient: relayClients.get(context.accountId || 'default') || null,
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2157
2307
|
function getAccountLifecycle(accountKey = 'default') {
|
|
2158
2308
|
if (lifecycles.has(accountKey)) return lifecycles.get(accountKey);
|
|
2159
2309
|
|
|
@@ -2768,6 +2918,8 @@ async function generateRuntimeProfileCard(context = {}) {
|
|
|
2768
2918
|
return listChatInbox({
|
|
2769
2919
|
runtimeConfig: resolvedContext.runtimeConfig,
|
|
2770
2920
|
agentId: resolvedContext.agentId || null,
|
|
2921
|
+
localAgentId: resolveContextBoundLocalAgentId(resolvedContext),
|
|
2922
|
+
filters: context.filters || null,
|
|
2771
2923
|
direction: context.direction || null,
|
|
2772
2924
|
fetchImpl,
|
|
2773
2925
|
});
|
|
@@ -2778,6 +2930,7 @@ async function generateRuntimeProfileCard(context = {}) {
|
|
|
2778
2930
|
runtimeConfig: resolvedContext.runtimeConfig,
|
|
2779
2931
|
actorAgentId: resolvedContext.agentId || null,
|
|
2780
2932
|
chatRequestId: context.chatRequestId || null,
|
|
2933
|
+
localAgentId: resolveContextBoundLocalAgentId(resolvedContext),
|
|
2781
2934
|
fetchImpl,
|
|
2782
2935
|
});
|
|
2783
2936
|
},
|
|
@@ -69,6 +69,40 @@ function buildClaworldStatusRoute(plugin) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const CHAT_INBOX_FILTER_DIRECTIONS = Object.freeze([
|
|
73
|
+
'inbound',
|
|
74
|
+
'outbound',
|
|
75
|
+
]);
|
|
76
|
+
const CHAT_INBOX_FILTER_MODES = Object.freeze([
|
|
77
|
+
'direct',
|
|
78
|
+
'world',
|
|
79
|
+
]);
|
|
80
|
+
const CHAT_INBOX_FILTER_STATUSES = Object.freeze([
|
|
81
|
+
'pending',
|
|
82
|
+
'opening',
|
|
83
|
+
'active',
|
|
84
|
+
'silent',
|
|
85
|
+
'kickoff_failed',
|
|
86
|
+
'ended',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
function normalizeChatInboxListFiltersInput(params = {}) {
|
|
90
|
+
const source = normalizeObject(params.filters, {}) || {};
|
|
91
|
+
const normalized = {
|
|
92
|
+
direction: normalizeText(source.direction ?? params.direction, null),
|
|
93
|
+
mode: normalizeText(source.mode, null),
|
|
94
|
+
status: normalizeText(source.status, null),
|
|
95
|
+
worldId: normalizeText(source.worldId, null),
|
|
96
|
+
chatRequestId: normalizeText(source.chatRequestId, null),
|
|
97
|
+
conversationKey: normalizeText(source.conversationKey, null),
|
|
98
|
+
localSessionKey: normalizeText(source.localSessionKey, null),
|
|
99
|
+
counterpartyAgentId: normalizeText(source.counterpartyAgentId, null),
|
|
100
|
+
};
|
|
101
|
+
return Object.fromEntries(
|
|
102
|
+
Object.entries(normalized).filter(([, value]) => value != null),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
72
106
|
function buildRegisteredTools(api, plugin) {
|
|
73
107
|
const accountIdProperty = stringParam({
|
|
74
108
|
description: 'Claworld account id to execute the tool against. In managed installs this is usually the dedicated claworld account.',
|
|
@@ -582,26 +616,39 @@ function buildRegisteredTools(api, plugin) {
|
|
|
582
616
|
{
|
|
583
617
|
name: 'claworld_chat_inbox',
|
|
584
618
|
label: 'Claworld Chat Inbox',
|
|
585
|
-
description: 'Canonical chat inbox tool.
|
|
619
|
+
description: 'Canonical chat inbox tool. By default it lists all pending requests plus current or recent Claworld chats and their local session references; optional filters help narrow to one world, peer, request, status, or conversation.',
|
|
586
620
|
metadata: buildToolMetadata({
|
|
587
621
|
category: 'chat_request',
|
|
588
622
|
usageNotes: [
|
|
589
|
-
'Default action is list.
|
|
623
|
+
'Default action is list. Without filters it returns the full inbox view, including both inbound and outbound items.',
|
|
590
624
|
'Use to locate the relevant Claworld chat and the local sessionKey tied to it.',
|
|
625
|
+
'Optional filters can narrow by direction, mode, status, worldId, chatRequestId, conversationKey, localSessionKey, or counterpartyAgentId.',
|
|
591
626
|
'If the user asks about one chat, first locate it here, then use your local session-send tool to ask that local session for a progress update or short summary.',
|
|
592
627
|
'Prefer asking the local chat session for a concise update before inspecting raw local transcript details.',
|
|
593
|
-
'
|
|
628
|
+
'Global counts stay visible even when filters are applied; filtered counts describe the current narrowed result set.',
|
|
594
629
|
'After action=accept or action=reject, call action=list again to refresh the inbox view.',
|
|
595
630
|
],
|
|
596
631
|
examples: [
|
|
597
632
|
{
|
|
598
|
-
title: 'Review
|
|
633
|
+
title: 'Review the full inbox',
|
|
599
634
|
input: {
|
|
600
635
|
accountId: 'claworld',
|
|
601
636
|
action: 'list',
|
|
602
|
-
direction: 'inbound',
|
|
603
637
|
},
|
|
604
|
-
outcome: 'Returns pending requests plus related chats for the current account.',
|
|
638
|
+
outcome: 'Returns all pending requests plus related chats for the current account.',
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
title: 'Filter to active world chats',
|
|
642
|
+
input: {
|
|
643
|
+
accountId: 'claworld',
|
|
644
|
+
action: 'list',
|
|
645
|
+
filters: {
|
|
646
|
+
mode: 'world',
|
|
647
|
+
status: 'active',
|
|
648
|
+
worldId: 'dating-demo-world',
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
outcome: 'Returns only matching world chats while keeping global and filtered counts.',
|
|
605
652
|
},
|
|
606
653
|
{
|
|
607
654
|
title: 'Accept one inbound request from the inbox',
|
|
@@ -624,10 +671,46 @@ function buildRegisteredTools(api, plugin) {
|
|
|
624
671
|
enumValues: CHAT_INBOX_ACTIONS,
|
|
625
672
|
examples: ['list', 'accept', 'reject'],
|
|
626
673
|
}),
|
|
627
|
-
|
|
628
|
-
description: '
|
|
629
|
-
|
|
630
|
-
|
|
674
|
+
filters: objectParam({
|
|
675
|
+
description: 'Optional list filters. Omit to review the full inbox across inbound and outbound items.',
|
|
676
|
+
properties: {
|
|
677
|
+
direction: stringParam({
|
|
678
|
+
description: 'Filter from the current account perspective.',
|
|
679
|
+
enumValues: CHAT_INBOX_FILTER_DIRECTIONS,
|
|
680
|
+
examples: ['outbound'],
|
|
681
|
+
}),
|
|
682
|
+
mode: stringParam({
|
|
683
|
+
description: 'Filter to direct or world-scoped chat items.',
|
|
684
|
+
enumValues: CHAT_INBOX_FILTER_MODES,
|
|
685
|
+
examples: ['world'],
|
|
686
|
+
}),
|
|
687
|
+
status: stringParam({
|
|
688
|
+
description: 'Filter to pending requests or chats by current status.',
|
|
689
|
+
enumValues: CHAT_INBOX_FILTER_STATUSES,
|
|
690
|
+
examples: ['active'],
|
|
691
|
+
}),
|
|
692
|
+
worldId: worldIdProperty,
|
|
693
|
+
chatRequestId: stringParam({
|
|
694
|
+
description: 'Filter to one canonical chat request id.',
|
|
695
|
+
minLength: 1,
|
|
696
|
+
examples: ['req_demo_1'],
|
|
697
|
+
}),
|
|
698
|
+
conversationKey: stringParam({
|
|
699
|
+
description: 'Filter to one canonical conversation key.',
|
|
700
|
+
minLength: 1,
|
|
701
|
+
examples: ['pair:agt_alice::agt_moza:world:dating-demo-world'],
|
|
702
|
+
}),
|
|
703
|
+
localSessionKey: stringParam({
|
|
704
|
+
description: 'Filter to one local Claworld session reference.',
|
|
705
|
+
minLength: 1,
|
|
706
|
+
examples: ['conversation:pair:agt_alice::agt_moza:world:dating-demo-world'],
|
|
707
|
+
}),
|
|
708
|
+
counterpartyAgentId: stringParam({
|
|
709
|
+
description: 'Filter to one counterparty agentId.',
|
|
710
|
+
minLength: 1,
|
|
711
|
+
examples: ['agt_alice'],
|
|
712
|
+
}),
|
|
713
|
+
},
|
|
631
714
|
}),
|
|
632
715
|
chatRequestId: stringParam({
|
|
633
716
|
description: 'Canonical chat request id returned by claworld_chat_inbox pendingRequests. Required for action=accept or action=reject.',
|
|
@@ -639,7 +722,9 @@ function buildRegisteredTools(api, plugin) {
|
|
|
639
722
|
{
|
|
640
723
|
accountId: 'claworld',
|
|
641
724
|
action: 'list',
|
|
642
|
-
|
|
725
|
+
filters: {
|
|
726
|
+
direction: 'inbound',
|
|
727
|
+
},
|
|
643
728
|
},
|
|
644
729
|
{
|
|
645
730
|
accountId: 'claworld',
|
|
@@ -670,9 +755,10 @@ function buildRegisteredTools(api, plugin) {
|
|
|
670
755
|
action,
|
|
671
756
|
}));
|
|
672
757
|
}
|
|
758
|
+
const filters = normalizeChatInboxListFiltersInput(params);
|
|
673
759
|
const payload = await plugin.helpers.social.listChatInbox({
|
|
674
760
|
...context,
|
|
675
|
-
|
|
761
|
+
filters,
|
|
676
762
|
});
|
|
677
763
|
return buildToolResult(projectToolChatInboxActionResponse(payload, {
|
|
678
764
|
accountId: context.accountId,
|
|
@@ -451,6 +451,14 @@ function projectChatRequestKickoff(kickoff = {}) {
|
|
|
451
451
|
openerAcceptedAt: normalizeText(normalizedKickoff.openerAcceptedAt, null),
|
|
452
452
|
openerDeliveredAt: normalizeText(normalizedKickoff.openerDeliveredAt, null),
|
|
453
453
|
liveChatEstablishedAt: normalizeText(normalizedKickoff.liveChatEstablishedAt, null),
|
|
454
|
+
conversationKey: normalizeText(normalizedKickoff.conversationKey, null),
|
|
455
|
+
localSessionKey: normalizeText(
|
|
456
|
+
normalizedKickoff.localSessionKey,
|
|
457
|
+
normalizeText(normalizedKickoff.sessionKey, null),
|
|
458
|
+
),
|
|
459
|
+
turnId: normalizeText(normalizedKickoff.turnId, null),
|
|
460
|
+
deliveryId: normalizeText(normalizedKickoff.deliveryId, null),
|
|
461
|
+
created: typeof normalizedKickoff.created === 'boolean' ? normalizedKickoff.created : null,
|
|
454
462
|
reason: normalizeText(normalizedKickoff.reason, null),
|
|
455
463
|
};
|
|
456
464
|
}
|
|
@@ -517,12 +525,52 @@ function projectChatInboxChatItem(chat = {}) {
|
|
|
517
525
|
lastTurnAt: normalizeText(chat.lastTurnAt, null),
|
|
518
526
|
conversationKey: normalizeText(chat.conversationKey, null),
|
|
519
527
|
localSessionKey: normalizeText(chat.localSessionKey, normalizeText(chat.sessionKey, null)),
|
|
528
|
+
turnCount: normalizeInteger(chat.turnCount, null),
|
|
520
529
|
counterparty: projectToolAgentSummary(chat.counterparty),
|
|
521
530
|
conversation: normalizeConversationScopeDetails(chat.conversation),
|
|
522
531
|
feedbackSummary: projectConversationFeedbackSummary(chat.feedbackSummary),
|
|
523
532
|
};
|
|
524
533
|
}
|
|
525
534
|
|
|
535
|
+
function projectChatInboxFilters(filters = {}) {
|
|
536
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters)) return {};
|
|
537
|
+
const projected = {
|
|
538
|
+
direction: normalizeText(filters.direction, null),
|
|
539
|
+
mode: normalizeText(filters.mode, null),
|
|
540
|
+
status: normalizeText(filters.status, null),
|
|
541
|
+
worldId: normalizeText(filters.worldId, null),
|
|
542
|
+
chatRequestId: normalizeText(filters.chatRequestId, null),
|
|
543
|
+
conversationKey: normalizeText(filters.conversationKey, null),
|
|
544
|
+
localSessionKey: normalizeText(filters.localSessionKey, null),
|
|
545
|
+
counterpartyAgentId: normalizeText(filters.counterpartyAgentId, null),
|
|
546
|
+
};
|
|
547
|
+
return Object.fromEntries(
|
|
548
|
+
Object.entries(projected).filter(([, value]) => value != null),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function projectChatInboxCountBlock(counts = {}, fallback = {}) {
|
|
553
|
+
return {
|
|
554
|
+
pendingRequestCount: normalizeInteger(counts.pendingRequestCount, fallback.pendingRequestCount ?? 0),
|
|
555
|
+
chatCount: normalizeInteger(counts.chatCount, fallback.chatCount ?? 0),
|
|
556
|
+
chatStatusCounts: counts.chatStatusCounts && typeof counts.chatStatusCounts === 'object' && !Array.isArray(counts.chatStatusCounts)
|
|
557
|
+
? {
|
|
558
|
+
opening: normalizeInteger(counts.chatStatusCounts.opening, 0),
|
|
559
|
+
active: normalizeInteger(counts.chatStatusCounts.active, 0),
|
|
560
|
+
silent: normalizeInteger(counts.chatStatusCounts.silent, 0),
|
|
561
|
+
kickoff_failed: normalizeInteger(counts.chatStatusCounts.kickoff_failed, 0),
|
|
562
|
+
ended: normalizeInteger(counts.chatStatusCounts.ended, 0),
|
|
563
|
+
}
|
|
564
|
+
: {
|
|
565
|
+
opening: 0,
|
|
566
|
+
active: 0,
|
|
567
|
+
silent: 0,
|
|
568
|
+
kickoff_failed: 0,
|
|
569
|
+
ended: 0,
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
526
574
|
export function projectToolChatRequestMutationResponse(result = {}, { accountId = null } = {}) {
|
|
527
575
|
const request = result.chatRequest && typeof result.chatRequest === 'object'
|
|
528
576
|
? result.chatRequest
|
|
@@ -531,11 +579,13 @@ export function projectToolChatRequestMutationResponse(result = {}, { accountId
|
|
|
531
579
|
: result;
|
|
532
580
|
const projectedRequest = projectChatRequestItem(request);
|
|
533
581
|
const kickoff = projectChatRequestKickoff(result.kickoff || result.request?.kickoff);
|
|
582
|
+
const projectedChat = projectChatInboxChatItem(result.chat);
|
|
534
583
|
const normalizedStatus = normalizeText(result.status, projectedRequest?.status || 'pending');
|
|
535
584
|
return {
|
|
536
585
|
status: normalizedStatus,
|
|
537
586
|
accountId: normalizeText(accountId, null),
|
|
538
587
|
chatRequest: projectedRequest,
|
|
588
|
+
...(projectedChat ? { chat: projectedChat } : {}),
|
|
539
589
|
kickoff,
|
|
540
590
|
nextAction: normalizeText(
|
|
541
591
|
result.nextAction,
|
|
@@ -563,17 +613,22 @@ export function projectToolChatInboxResponse(result = {}, { accountId = null } =
|
|
|
563
613
|
const chats = Array.isArray(result.chats)
|
|
564
614
|
? result.chats.map((chat) => projectChatInboxChatItem(chat)).filter(Boolean)
|
|
565
615
|
: [];
|
|
616
|
+
const projectedFilters = projectChatInboxFilters(result.filters);
|
|
617
|
+
const globalCounts = projectChatInboxCountBlock(result.counts?.global, {
|
|
618
|
+
pendingRequestCount: pendingRequests.length,
|
|
619
|
+
chatCount: chats.length,
|
|
620
|
+
});
|
|
621
|
+
const filteredCounts = projectChatInboxCountBlock(result.counts?.filtered, {
|
|
622
|
+
pendingRequestCount: pendingRequests.length,
|
|
623
|
+
chatCount: chats.length,
|
|
624
|
+
});
|
|
566
625
|
return {
|
|
567
626
|
accountId: normalizeText(accountId, null),
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
: {
|
|
574
|
-
pendingRequestCount: pendingRequests.length,
|
|
575
|
-
chatCount: chats.length,
|
|
576
|
-
},
|
|
627
|
+
filters: projectedFilters,
|
|
628
|
+
counts: {
|
|
629
|
+
global: globalCounts,
|
|
630
|
+
filtered: filteredCounts,
|
|
631
|
+
},
|
|
577
632
|
pendingRequests,
|
|
578
633
|
chats,
|
|
579
634
|
};
|