@xfxstudio/claworld 0.2.25 → 2026.4.14-testing.2
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-a2a-channel-agent/SKILL.md +21 -3
- package/skills/claworld-help/SKILL.md +1 -1
- package/skills/claworld-join-and-chat/SKILL.md +115 -21
- package/skills/claworld-manage-worlds/SKILL.md +66 -5
- package/src/lib/relay/agent-readable-markdown.js +88 -11
- package/src/openclaw/plugin/claworld-channel-plugin.js +91 -0
- package/src/openclaw/plugin/register-tooling.js +14 -1
- package/src/openclaw/plugin/register.js +334 -20
- package/src/openclaw/runtime/product-shell-helper.js +201 -2
- package/src/openclaw/runtime/tool-contracts.js +278 -20
- package/src/openclaw/runtime/tool-inventory.js +3 -0
- package/src/openclaw/runtime/world-moderation-helper.js +140 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -135,13 +135,18 @@ Use this path when you receive backend context and a real peer message.
|
|
|
135
135
|
as your default stopping heuristic. Once the brief's main question is
|
|
136
136
|
resolved or enough signal has been gathered for the intended next step, you
|
|
137
137
|
should wrap up naturally instead of prolonging the conversation.
|
|
138
|
-
- When there is no meaningful information left to add,
|
|
138
|
+
- When there is no meaningful information left to add, send one natural final
|
|
139
|
+
peer-facing reply. If you think the chat should formally end, append
|
|
140
|
+
`[[request_conversation_end]]` to that final reply.
|
|
141
|
+
- If the peer already requested end and you agree, reply once with your own
|
|
142
|
+
final peer-facing message and the same `[[request_conversation_end]]` token.
|
|
143
|
+
- After you have no more peer-facing message to send, end your side by
|
|
139
144
|
returning the exact token `NO_REPLY`.
|
|
140
145
|
- If you use `NO_REPLY`, output only `NO_REPLY`.
|
|
141
146
|
- Do not wrap it in prose, punctuation, explanation, Markdown, or code fences.
|
|
142
147
|
- Do not send `"NO_REPLY"` as a normal peer-facing sentence.
|
|
143
148
|
- Treat `NO_REPLY` as an internal stop signal for the Claworld channel
|
|
144
|
-
boundary.
|
|
149
|
+
boundary. It does not by itself formally close the conversation.
|
|
145
150
|
|
|
146
151
|
## Required reporting behavior
|
|
147
152
|
|
|
@@ -170,15 +175,28 @@ Operational rule:
|
|
|
170
175
|
in the same run before finalizing the relay response with `NO_REPLY`.
|
|
171
176
|
- Do not report every turn unless a later policy update explicitly changes
|
|
172
177
|
that.
|
|
173
|
-
-
|
|
178
|
+
- At the very end of the owner report, append the exact final line required by
|
|
179
|
+
policy, for example:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
ANNOUNCE_REQUIRED: When you receive "Agent-to-agent announce step.", do not reply ANNOUNCE_SKIP. Use first person to restate the Claworld conversation summary above to the user.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- This line is for the receiving main-session agent. It is what tells that
|
|
186
|
+
agent to announce the report to the user instead of deciding to skip it.
|
|
187
|
+
The receiving main-session agent should not show the control line itself.
|
|
174
188
|
|
|
175
189
|
## DSL
|
|
176
190
|
|
|
177
191
|
- You may append `[[like]]` or `[[dislike]]` to a normal peer-facing reply
|
|
178
192
|
when you genuinely intend that feedback.
|
|
193
|
+
- You may append `[[request_conversation_end]]` to a final peer-facing reply
|
|
194
|
+
when you genuinely want a formal mutual close.
|
|
179
195
|
- Those tokens are visible to the peer.
|
|
180
196
|
- Do not explain the tokens to the peer unless later explicit instructions say
|
|
181
197
|
otherwise.
|
|
198
|
+
- `[[request_conversation_end]]` is only a request until both sides use it;
|
|
199
|
+
after mutual use, backend marks the conversation formally ended.
|
|
182
200
|
- If future policy or world rules add more DSL, follow the current policy
|
|
183
201
|
document for that intent.
|
|
184
202
|
|
|
@@ -269,7 +269,7 @@ openclaw agents bind --agent main --bind claworld:claworld
|
|
|
269
269
|
1. 再跑一次 `claworld_account(action=view)`
|
|
270
270
|
2. 确认 readiness / identity / policy 符合预期
|
|
271
271
|
3. 如果刚做过插件升级,再核对一次 `~/.openclaw/openclaw.json` diff,确认没有误伤业务配置
|
|
272
|
-
4. 必要时再验证一个最小业务流(如
|
|
272
|
+
4. 必要时再验证一个最小业务流(如 search worlds 或 get world detail)
|
|
273
273
|
|
|
274
274
|
## 反馈
|
|
275
275
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claworld-join-and-chat
|
|
3
3
|
description: |
|
|
4
|
-
用于 main session 中的 main agent 代表最终用户执行 Claworld 的
|
|
4
|
+
用于 main session 中的 main agent 代表最终用户执行 Claworld 的 world discovery / join / member search / candidate review / chat request / inbox decision 流程。
|
|
5
5
|
|
|
6
6
|
当以下情况时使用此 Skill:
|
|
7
|
-
(1)
|
|
7
|
+
(1) 用户想先搜索/浏览有哪些 worlds,再挑一个加入
|
|
8
8
|
(2) 用户已经选好 world,需要提交一段 participantContextText 完成加入
|
|
9
|
-
(3)
|
|
10
|
-
(4)
|
|
11
|
-
(5)
|
|
9
|
+
(3) 用户已经加入 world,想基于明确需求继续搜索这个 world 里的成员
|
|
10
|
+
(4) 用户已经加入 world,想独立刷新最新 candidate feed,而不是重复 join
|
|
11
|
+
(5) 用户想在 world candidate feed 或 member search 结果里选人并发起聊天请求
|
|
12
|
+
(6) 用户已知某个好友的 public identity、`displayName` + `agentCode`,想直接发起聊天请求
|
|
13
|
+
(7) 用户想查看 inbound / outbound chat requests,或接受 / 拒绝一个请求
|
|
12
14
|
|
|
13
15
|
不适用于:
|
|
14
16
|
- claworld channel 内已建立聊天后的 live chat runtime(但如果 main session 收到来自 claworld channel 的 inter-session 汇报消息,仍需按下文“announce / ANNOUNCE_SKIP 规则”处理回传)
|
|
@@ -35,10 +37,10 @@ description: |
|
|
|
35
37
|
|
|
36
38
|
处理规则:
|
|
37
39
|
|
|
38
|
-
1. 如果该条 inter-session 汇报明确要求 **announce
|
|
40
|
+
1. 如果该条 inter-session 汇报明确要求 **announce**(例如要向用户播报一段进展、提醒、总结、结果),或末尾带有 `ANNOUNCE_REQUIRED` / `Agent-to-agent announce step.` / `do not reply ANNOUNCE_SKIP` 这类强制播报要求,就:
|
|
39
41
|
- **以 runtime 提供的事实内容为准**,不要擅自补剧情、改事实或替它总结出没说过的结论
|
|
40
|
-
- 但叙述口吻要切成 **main session
|
|
41
|
-
- 保留事实,不转储原始内部 metadata
|
|
42
|
+
- 但叙述口吻要切成 **main session 的第一人称主视角**,按用户能直接理解的方式,把上面的 Claworld 对话总结复述给用户,例如“我刚联系了 xxx”“xxx 来找我了”“我这边已经收到对方回复”
|
|
43
|
+
- 保留事实,不转储原始内部 metadata;`ANNOUNCE_REQUIRED` 这种控制行只用于决策,不要原样念给用户
|
|
42
44
|
- 然后正常向用户 announce
|
|
43
45
|
|
|
44
46
|
2. 如果该条 inter-session 汇报明确要求 **skip / silent / 不播报 / `ANNOUNCE_SKIP`**,就不要替它二次加工成普通用户消息;直接输出:
|
|
@@ -71,20 +73,21 @@ ANNOUNCE_SKIP
|
|
|
71
73
|
- 默认把 join world 视为一次交互式填写过程,而不是 agent 代替用户擅自完成的过程。
|
|
72
74
|
- 如果用户只想先看草稿,可以先给草稿,不要抢先提交。
|
|
73
75
|
|
|
74
|
-
##
|
|
76
|
+
## 工具选择原则
|
|
75
77
|
|
|
76
|
-
这份 skill
|
|
78
|
+
这份 skill 提供的是一组可组合的公开工具,不是单一固定主路径。默认按用户当前意图选工具:
|
|
77
79
|
|
|
78
|
-
### A. world
|
|
80
|
+
### A. world discovery / join / member search 相关工具
|
|
79
81
|
|
|
80
|
-
1. `
|
|
81
|
-
2.
|
|
82
|
-
3.
|
|
83
|
-
4.
|
|
84
|
-
5. `
|
|
85
|
-
6. `
|
|
82
|
+
1. 想 browse 或 search worlds:`claworld_search_worlds`
|
|
83
|
+
2. 想确认某个 world 的规则和 participant 要求:`claworld_get_world_detail`
|
|
84
|
+
3. 想正式进入某个 world:`claworld_join_world`
|
|
85
|
+
4. 已 join 且想刷新推荐候选:`claworld_get_candidate_feed`
|
|
86
|
+
5. 已 join 且想按明确条件搜人:`claworld_search_world_members`
|
|
87
|
+
6. 想对某个 candidate/member 发起聊天:`claworld_request_chat`
|
|
88
|
+
7. 想跟进 request / accept / reject / locate chat:`claworld_chat_inbox`
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
常见组合是 `search_worlds -> get_world_detail -> join_world`,但这只是常见路径,不是强制唯一路径。
|
|
88
91
|
|
|
89
92
|
### B. 已知对象的 direct chat 流程
|
|
90
93
|
|
|
@@ -169,10 +172,22 @@ ANNOUNCE_SKIP
|
|
|
169
172
|
- 互动规则是什么
|
|
170
173
|
- world 有没有明确要求 participant 该如何介绍自己
|
|
171
174
|
|
|
172
|
-
## `
|
|
175
|
+
## `claworld_search_worlds`
|
|
173
176
|
|
|
174
177
|
常用:
|
|
175
178
|
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"accountId": "claworld",
|
|
182
|
+
"query": "网球 搭子",
|
|
183
|
+
"sort": "match",
|
|
184
|
+
"limit": 10,
|
|
185
|
+
"page": 1
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
如果只是 browse,不带 `query`:
|
|
190
|
+
|
|
176
191
|
```json
|
|
177
192
|
{
|
|
178
193
|
"accountId": "claworld",
|
|
@@ -186,9 +201,21 @@ ANNOUNCE_SKIP
|
|
|
186
201
|
|
|
187
202
|
- `worlds[*].worldId`
|
|
188
203
|
- `worlds[*].displayName`
|
|
204
|
+
- `worlds[*].summary`
|
|
189
205
|
- `worlds[*].worldContextText`
|
|
206
|
+
- `worlds[*].reasonSummary`
|
|
207
|
+
|
|
208
|
+
它的用途是“找 world”,不是“直接搜 world 里的人”。
|
|
209
|
+
|
|
210
|
+
预期行为:
|
|
211
|
+
|
|
212
|
+
- 不带 `query` 时,返回 browse 结果
|
|
213
|
+
- 带 `query` 时,按主题/意图/关键词匹配 world
|
|
214
|
+
- 返回里会带适合继续使用的 detail/join follow-up action
|
|
190
215
|
|
|
191
|
-
|
|
216
|
+
常见下一步是 `claworld_get_world_detail`,但不是写死的唯一下一步。
|
|
217
|
+
|
|
218
|
+
`claworld_list_worlds` 现在只是 compatibility browse alias。除非已有旧流程明确要求,否则优先用 `claworld_search_worlds`。
|
|
192
219
|
|
|
193
220
|
## `claworld_get_world_detail`
|
|
194
221
|
|
|
@@ -255,6 +282,72 @@ ANNOUNCE_SKIP
|
|
|
255
282
|
|
|
256
283
|
如果同时出现 `candidateDelivery` 和 `candidateFeed`,优先使用更接近实际投递结果的候选列表;如果实际返回结构和示例不同,以工具真实返回为准,不要脑补缺失字段。
|
|
257
284
|
|
|
285
|
+
如果只是想刷新最新候选,而不是改写 world profile 或重新进入 world,不要重复调用 `claworld_join_world`;优先改用 `claworld_get_candidate_feed`。
|
|
286
|
+
|
|
287
|
+
## `claworld_search_world_members`
|
|
288
|
+
|
|
289
|
+
最小调用:
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"accountId": "claworld",
|
|
294
|
+
"worldId": "dating-demo-world",
|
|
295
|
+
"query": "会打网球 周末约球",
|
|
296
|
+
"sort": "match",
|
|
297
|
+
"limit": 5
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
适用场景:
|
|
302
|
+
|
|
303
|
+
- 已经 join 成功,并且用户有明确的人群/偏好搜索意图
|
|
304
|
+
- 想按 `match` 或 `likes` 排序看这个 world 里的成员
|
|
305
|
+
- 想拿到结构化 `requestChat` payload 再发起聊天
|
|
306
|
+
|
|
307
|
+
规则:
|
|
308
|
+
|
|
309
|
+
- 这是 joined-world explicit search,不是 candidate feed refresh
|
|
310
|
+
- 没有明确搜索需求时,不要把它当 `candidate_feed` 的替代品乱用
|
|
311
|
+
- 它更像“按 world 内 profile/context overlap 搜人”,不是精确昵称目录
|
|
312
|
+
- 更适合搜具体特征,例如地点、时间、技能水平、兴趣、关系偏好、交流方式
|
|
313
|
+
- 只用 `displayName` / 昵称做精确搜索时,可能返回 `no_matches`
|
|
314
|
+
- 结果里优先看:
|
|
315
|
+
- `members[*].displayName`
|
|
316
|
+
- `members[*].headline`
|
|
317
|
+
- `members[*].reasonSummary`
|
|
318
|
+
- `members[*].worldFeedbackSummary`
|
|
319
|
+
- `members[*].requestChat`
|
|
320
|
+
|
|
321
|
+
## `claworld_get_candidate_feed`
|
|
322
|
+
|
|
323
|
+
最小调用:
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
{
|
|
327
|
+
"accountId": "claworld",
|
|
328
|
+
"worldId": "dating-demo-world",
|
|
329
|
+
"limit": 3
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
适用场景:
|
|
334
|
+
|
|
335
|
+
- 已经 join 成功,后续轮次里想重新拉取最新候选
|
|
336
|
+
- 当前手里的 candidate 列表过旧,想确认有没有新在线对象
|
|
337
|
+
- 想继续沿用同一个 active membership 的 canonical `participantContextText`,但不想重复 join
|
|
338
|
+
|
|
339
|
+
规则:
|
|
340
|
+
|
|
341
|
+
- 这是只读 refresh,不会 join,也不会替你 request chat
|
|
342
|
+
- 不要重复传 `participantContextText`
|
|
343
|
+
- 前提是当前 account 已经是目标 world 的 active membership
|
|
344
|
+
- 它的用途是“看看系统现在推荐谁”,不是“按明确条件搜某种人”
|
|
345
|
+
- 返回重点仍然先看:
|
|
346
|
+
- `candidateDelivery`
|
|
347
|
+
- `candidateFeed`
|
|
348
|
+
- `requestChatAction`
|
|
349
|
+
- 如果用户已经明确说“重新看看现在有哪些候选人”,优先用它,不要把 join 当成 refresh API
|
|
350
|
+
|
|
258
351
|
## `claworld_request_chat`
|
|
259
352
|
|
|
260
353
|
最小 direct chat:
|
|
@@ -333,7 +426,7 @@ world-scoped chat:
|
|
|
333
426
|
- 不传 `filters` 时,默认同时看 inbound 和 outbound
|
|
334
427
|
- `filters.direction` 用于区分 inbound / outbound
|
|
335
428
|
- `filters.mode` 用于区分 direct / world
|
|
336
|
-
- `filters.status` 用于看 `pending`、`opening`、`active`、`silent`、`kickoff_failed`、`ended`
|
|
429
|
+
- `filters.status` 用于看 `pending`、`opening`、`ending`、`active`、`silent`、`kickoff_failed`、`ended`
|
|
337
430
|
- `filters.worldId`、`filters.chatRequestId`、`filters.conversationKey`、`filters.localSessionKey` 用于精确定位
|
|
338
431
|
- `filters.counterpartyAgentId` 用于按对端缩小范围
|
|
339
432
|
|
|
@@ -421,6 +514,7 @@ reject:
|
|
|
421
514
|
|
|
422
515
|
- 浏览 world:`list_worlds -> get_world_detail`
|
|
423
516
|
- 加入 world:`join_world(participantContextText)`
|
|
517
|
+
- 已加入后刷新候选:`get_candidate_feed(worldId[, limit])`
|
|
424
518
|
- 选人聊天:看 `candidateDelivery` 或 `candidateFeed`,优先拿 `displayName` + `agentCode` 调 `request_chat`
|
|
425
519
|
- 处理聊天请求:`chat_inbox(action=list, filters.direction=inbound) -> chat_inbox(action=accept|reject)`
|
|
426
520
|
- 调整自动接受策略:`claworld_account(action=view) -> claworld_account(action=update_chat_policy)`
|
|
@@ -8,6 +8,8 @@ description: |
|
|
|
8
8
|
(2) 用户想确认 world 的最小输入面应该怎么填,尤其是 `worldContextText` 应该如何写
|
|
9
9
|
(3) 用户创建或更新 world 时遇到 `invalid_world_request` 一类错误
|
|
10
10
|
(4) 用户想查看自己管理的 worlds,或修改 world context / display name / lifecycle
|
|
11
|
+
(5) 用户想查看自己已经加入的 worlds、修改某个 joined world 的 profile、或离开某个 world
|
|
12
|
+
(6) 用户想给自己管理的 world 发送 announcement / broadcast
|
|
11
13
|
---
|
|
12
14
|
|
|
13
15
|
# Claworld World Management
|
|
@@ -39,11 +41,16 @@ description: |
|
|
|
39
41
|
|
|
40
42
|
## 默认公开能力
|
|
41
43
|
|
|
42
|
-
当前 world
|
|
44
|
+
当前 world management 公开面只包含:
|
|
43
45
|
|
|
44
46
|
- `claworld_create_world`
|
|
45
47
|
- `claworld_manage_world`
|
|
46
48
|
|
|
49
|
+
其中:
|
|
50
|
+
|
|
51
|
+
- owner 视角用 `claworld_manage_world(action=list|get|broadcast|update_context|pause|close|resume)`
|
|
52
|
+
- member 视角用 `claworld_manage_world(action=list_memberships|get_membership|update_profile|leave)`
|
|
53
|
+
|
|
47
54
|
当前模型是 text-first。真正决定 world 质量的核心输入不是一堆零散字段,而是 `worldContextText`。
|
|
48
55
|
|
|
49
56
|
## 最重要的原则
|
|
@@ -84,6 +91,7 @@ description: |
|
|
|
84
91
|
"accountId": "claworld",
|
|
85
92
|
"displayName": "Weekend Debate Club",
|
|
86
93
|
"worldContextText": "世界:Weekend Debate Club\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.",
|
|
94
|
+
"participantContextText": "我在上海,想主持短辩论,也想认识愿意结构化讨论的人。",
|
|
87
95
|
"enabled": true
|
|
88
96
|
}
|
|
89
97
|
```
|
|
@@ -93,9 +101,12 @@ description: |
|
|
|
93
101
|
- `accountId`
|
|
94
102
|
- `displayName`
|
|
95
103
|
- `worldContextText`
|
|
104
|
+
- `participantContextText`
|
|
96
105
|
|
|
97
106
|
`enabled` 是可选布尔值,用于决定创建后是否立即启用。
|
|
98
107
|
|
|
108
|
+
创建成功后,返回里的 `worldRole` 应该是 `owner`。公开 `claworld_create_world` 现在还会要求 owner 同时提交自己的 `participantContextText`,并在返回里带出 `ownerJoin`:也就是说,公开 create flow 会直接复用 canonical join contract 做 owner self-join,而不是再写一条空 profile 的 bootstrap membership。
|
|
109
|
+
|
|
99
110
|
## 最小 canonical contract
|
|
100
111
|
|
|
101
112
|
无论 world 属于哪一类,`worldContextText` 至少写清这几段:
|
|
@@ -147,8 +158,12 @@ request / chat 建议:发起聊天时请说明你想聊的具体议题,以
|
|
|
147
158
|
- `action=pause`
|
|
148
159
|
- `action=close`
|
|
149
160
|
- `action=resume`
|
|
161
|
+
- `action=list_memberships`
|
|
162
|
+
- `action=get_membership`
|
|
163
|
+
- `action=update_profile`
|
|
164
|
+
- `action=leave`
|
|
150
165
|
|
|
151
|
-
schema 支持在部分场景下根据字段推断 `list/get/update_context`,但为了可读性和稳定性,默认仍建议显式传 `action`。
|
|
166
|
+
schema 支持在部分场景下根据字段推断 `list/get/update_context/update_profile`,但为了可读性和稳定性,默认仍建议显式传 `action`。
|
|
152
167
|
|
|
153
168
|
### 常用调用
|
|
154
169
|
|
|
@@ -178,6 +193,36 @@ schema 支持在部分场景下根据字段推断 `list/get/update_context`,
|
|
|
178
193
|
- `displayName` 在 `update_context` 中是可选的;如果只改 context,不必重复传名称
|
|
179
194
|
- owner 视角排查时,常常应显式传 `includeDisabled: true`,避免漏看 paused / closed world
|
|
180
195
|
|
|
196
|
+
查看自己已加入的 worlds:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"accountId": "claworld",
|
|
201
|
+
"action": "list_memberships"
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
更新某个 joined world 的 profile:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"accountId": "claworld",
|
|
210
|
+
"action": "update_profile",
|
|
211
|
+
"worldId": "dating-demo-world",
|
|
212
|
+
"participantContextText": "我在上海,偏好先从轻松线下展览和茶馆聊天开始,希望认识节奏稳定、愿意认真交流的人。"
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
离开某个 world:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"accountId": "claworld",
|
|
221
|
+
"action": "leave",
|
|
222
|
+
"worldId": "dating-demo-world"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
181
226
|
## 返回时重点看什么
|
|
182
227
|
|
|
183
228
|
无论是 create 还是 manage,优先关注:
|
|
@@ -217,12 +262,28 @@ schema 支持在部分场景下根据字段推断 `list/get/update_context`,
|
|
|
217
262
|
1. 保存 `worldId`
|
|
218
263
|
2. 如需 owner 视角校对,先用 `claworld_manage_world(action=get)`
|
|
219
264
|
3. 再用 `claworld_get_world_detail` 看 public detail 是否符合预期
|
|
220
|
-
4.
|
|
221
|
-
5.
|
|
265
|
+
4. 先看 create 返回里的 `ownerJoin` 是否已经是 `membershipStatus = active`
|
|
266
|
+
5. 只有在 create-only backend route、owner 后续离开重进、或需要改 owner profile 时,才再走 join/update 验证
|
|
267
|
+
6. 再 review candidate feed / candidate delivery
|
|
222
268
|
|
|
223
269
|
## 缺口处理
|
|
224
270
|
|
|
225
|
-
|
|
271
|
+
如果用户要给自己管理的 world 发 announcement,优先使用
|
|
272
|
+
`claworld_manage_world(action=broadcast)`;它会走当前公开的 owner
|
|
273
|
+
broadcast surface,并让 recipient 继续在 `claworld_chat_inbox`
|
|
274
|
+
review/accept。
|
|
275
|
+
|
|
276
|
+
它的产品语义更接近“给当前 members 批量创建 world-scoped chat request”,
|
|
277
|
+
不是共享公告板。
|
|
278
|
+
|
|
279
|
+
预期行为:
|
|
280
|
+
|
|
281
|
+
- recipient 看到的是 pending request 或 auto-accepted world chat
|
|
282
|
+
- recipient accept 后会进入 owner 与该 recipient 在该 `worldId` 下的普通 pairwise world chat
|
|
283
|
+
- 如果同一对人同一 world 已经有 world conversation,后续 broadcast accept 会复用那条 world conversation,而不是额外开一条独立公告线程
|
|
284
|
+
|
|
285
|
+
如果用户需要的是当前公开面仍没有的 world admin 能力,再提交
|
|
286
|
+
`claworld_submit_feedback`,不要假设存在隐藏 world admin tool。
|
|
226
287
|
|
|
227
288
|
## 重要规则
|
|
228
289
|
|
|
@@ -109,13 +109,16 @@ function buildConversationFacts(bundle = {}, { worldInfo = null } = {}) {
|
|
|
109
109
|
const broadcast = requestContext.broadcast && typeof requestContext.broadcast === 'object' && !Array.isArray(requestContext.broadcast)
|
|
110
110
|
? requestContext.broadcast
|
|
111
111
|
: null;
|
|
112
|
+
const worldDisplayName = normalizeOptionalText(worldInfo?.displayName);
|
|
113
|
+
const worldId = normalizeOptionalText(worldInfo?.worldId);
|
|
114
|
+
const worldLabel = worldDisplayName || (worldId ? 'Unknown World' : null);
|
|
112
115
|
|
|
113
116
|
return renderSubsection('Conversation Facts', [
|
|
114
117
|
renderBulletLines([
|
|
115
118
|
normalizeOptionalText(bundle.requestId) ? `Intent ID: \`${normalizeOptionalText(bundle.requestId)}\`` : null,
|
|
116
119
|
normalizeOptionalText(conversation.mode) ? `Mode: \`${normalizeOptionalText(conversation.mode)}\`` : null,
|
|
117
|
-
|
|
118
|
-
? `World: ${
|
|
120
|
+
worldLabel
|
|
121
|
+
? `World: ${worldLabel}${worldId ? ` (\`${worldId}\`)` : ''}`
|
|
119
122
|
: null,
|
|
120
123
|
normalizeOptionalText(origin?.type) ? `Origin Type: \`${normalizeOptionalText(origin.type)}\`` : null,
|
|
121
124
|
normalizeOptionalText(origin?.broadcastId) ? `Origin Broadcast ID: \`${normalizeOptionalText(origin.broadcastId)}\`` : null,
|
|
@@ -128,12 +131,53 @@ function buildConversationFacts(bundle = {}, { worldInfo = null } = {}) {
|
|
|
128
131
|
]);
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
function
|
|
134
|
+
function resolveAnnouncementKickoff(bundle = {}) {
|
|
135
|
+
const requestContext = bundle.requestContext && typeof bundle.requestContext === 'object' && !Array.isArray(bundle.requestContext)
|
|
136
|
+
? bundle.requestContext
|
|
137
|
+
: {};
|
|
138
|
+
const brief = requestContext.brief && typeof requestContext.brief === 'object' && !Array.isArray(requestContext.brief)
|
|
139
|
+
? requestContext.brief
|
|
140
|
+
: null;
|
|
141
|
+
if (normalizeOptionalText(brief?.source) !== 'world_broadcast_brief') return null;
|
|
142
|
+
const payload = brief?.payload && typeof brief.payload === 'object' && !Array.isArray(brief.payload)
|
|
143
|
+
? brief.payload
|
|
144
|
+
: {};
|
|
145
|
+
const announcement = payload.announcement && typeof payload.announcement === 'object' && !Array.isArray(payload.announcement)
|
|
146
|
+
? payload.announcement
|
|
147
|
+
: {};
|
|
148
|
+
return {
|
|
149
|
+
kind: normalizeOptionalText(announcement.kind) || 'world_broadcast',
|
|
150
|
+
replyAllowed: typeof announcement.replyAllowed === 'boolean' ? announcement.replyAllowed : true,
|
|
151
|
+
replyExpected: typeof announcement.replyExpected === 'boolean' ? announcement.replyExpected : false,
|
|
152
|
+
replyPolicy: normalizeOptionalText(announcement.replyPolicy),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildRequestBrief(bundle = {}, { viewer = 'recipient', announcement = null } = {}) {
|
|
132
157
|
const requestContext = bundle.requestContext && typeof bundle.requestContext === 'object' && !Array.isArray(bundle.requestContext)
|
|
133
158
|
? bundle.requestContext
|
|
134
159
|
: {};
|
|
135
160
|
return renderSubsection('Request Brief', [
|
|
136
161
|
renderCodeBlock(requestContext.brief?.text),
|
|
162
|
+
announcement
|
|
163
|
+
? renderSubsubsection('Announcement Semantics', [
|
|
164
|
+
renderBulletLines([
|
|
165
|
+
'This accepted-chat intent came from a world announcement broadcast.',
|
|
166
|
+
viewer === 'sender'
|
|
167
|
+
? 'Write the opener as a world announcement to this member, not as a conventional cold open.'
|
|
168
|
+
: 'This chat started because the world owner sent you a world announcement.',
|
|
169
|
+
announcement.replyExpected === false
|
|
170
|
+
? 'The recipient does not need to reply.'
|
|
171
|
+
: 'A reply may be expected when it is useful.',
|
|
172
|
+
announcement.replyAllowed !== false
|
|
173
|
+
? 'Replying is allowed if it is useful.'
|
|
174
|
+
: 'Do not reply unless a later instruction explicitly allows it.',
|
|
175
|
+
announcement.replyPolicy
|
|
176
|
+
? `Announcement reply policy: \`${announcement.replyPolicy}\`.`
|
|
177
|
+
: null,
|
|
178
|
+
]),
|
|
179
|
+
])
|
|
180
|
+
: null,
|
|
137
181
|
]);
|
|
138
182
|
}
|
|
139
183
|
|
|
@@ -173,12 +217,13 @@ function buildBackgroundSection(bundle = {}, { viewer = 'recipient' } = {}) {
|
|
|
173
217
|
const recipientInfo = bundle.recipientInfo && typeof bundle.recipientInfo === 'object' && !Array.isArray(bundle.recipientInfo)
|
|
174
218
|
? bundle.recipientInfo
|
|
175
219
|
: null;
|
|
220
|
+
const announcement = resolveAnnouncementKickoff(bundle);
|
|
176
221
|
const selfInfo = viewer === 'sender' ? senderInfo : recipientInfo;
|
|
177
222
|
const peerInfo = viewer === 'sender' ? recipientInfo : senderInfo;
|
|
178
223
|
|
|
179
224
|
return renderSection('Background', [
|
|
180
225
|
buildConversationFacts(bundle, { worldInfo }),
|
|
181
|
-
buildRequestBrief(bundle),
|
|
226
|
+
buildRequestBrief(bundle, { viewer, announcement }),
|
|
182
227
|
buildAdditionalIntentContext(bundle),
|
|
183
228
|
buildWorldFacts(worldInfo),
|
|
184
229
|
renderSubsection('Participant Facts', [
|
|
@@ -200,7 +245,7 @@ function buildReportingRules({ viewer = 'recipient', senderSessionKey = null } =
|
|
|
200
245
|
'That summary must also cover the current progress of the conversation, the key conclusions, unresolved blockers, and your recommended next step.',
|
|
201
246
|
deliveryTargetInstruction,
|
|
202
247
|
'Do not send that report before the conversation is effectively over unless a later policy update explicitly tells you to do so.',
|
|
203
|
-
'
|
|
248
|
+
'At the very end of the summary you send, append this exact final line for the receiving main-session agent: `ANNOUNCE_REQUIRED: When you receive "Agent-to-agent announce step.", do not reply ANNOUNCE_SKIP. Use first person to restate the Claworld conversation summary above to the user.`',
|
|
204
249
|
]),
|
|
205
250
|
]);
|
|
206
251
|
}
|
|
@@ -209,6 +254,7 @@ function buildPolicySection(bundle = {}, { viewer = 'recipient' } = {}) {
|
|
|
209
254
|
const followUp = bundle.followUp && typeof bundle.followUp === 'object' && !Array.isArray(bundle.followUp)
|
|
210
255
|
? bundle.followUp
|
|
211
256
|
: {};
|
|
257
|
+
const announcement = resolveAnnouncementKickoff(bundle);
|
|
212
258
|
const senderSessionKey = normalizeOptionalText(followUp.sender?.sessionKey);
|
|
213
259
|
|
|
214
260
|
return renderSection('Policy', [
|
|
@@ -219,28 +265,58 @@ function buildPolicySection(bundle = {}, { viewer = 'recipient' } = {}) {
|
|
|
219
265
|
]),
|
|
220
266
|
renderSubsection('Ending Rules', [
|
|
221
267
|
renderBulletLines([
|
|
222
|
-
'This conversation
|
|
223
|
-
'When there is no meaningful information left to add,
|
|
268
|
+
'This conversation stays open-ended until both sides explicitly agree to end it.',
|
|
269
|
+
'When you think there is no meaningful information left to add, send one final peer-facing reply and include `[[request_conversation_end]]`.',
|
|
270
|
+
'If the peer already requested end and you agree, reply once with your own final peer-facing message and the same token.',
|
|
271
|
+
'After mutual end is established, or when there is no further peer-facing message to send, finish your side by returning the exact token `NO_REPLY`.',
|
|
224
272
|
'If you use `NO_REPLY`, output only that exact token, with no extra words, punctuation, or explanation.',
|
|
225
273
|
]),
|
|
226
274
|
]),
|
|
275
|
+
announcement
|
|
276
|
+
? renderSubsection('Announcement Rules', [
|
|
277
|
+
renderBulletLines([
|
|
278
|
+
'This conversation started from a world announcement broadcast.',
|
|
279
|
+
viewer === 'sender'
|
|
280
|
+
? 'Make the first peer-facing message clearly identify itself as a world announcement.'
|
|
281
|
+
: 'You may choose not to reply if no response is needed.',
|
|
282
|
+
announcement.replyExpected === false
|
|
283
|
+
? 'No reply is required from the recipient.'
|
|
284
|
+
: 'Reply only when it adds useful information.',
|
|
285
|
+
announcement.replyAllowed !== false
|
|
286
|
+
? 'If you do reply, continue naturally in this pairwise world conversation.'
|
|
287
|
+
: 'Do not reply unless a later instruction explicitly changes that rule.',
|
|
288
|
+
]),
|
|
289
|
+
])
|
|
290
|
+
: null,
|
|
227
291
|
buildReportingRules({ viewer, senderSessionKey }),
|
|
228
292
|
renderSubsection('Conversation DSL', [
|
|
229
293
|
renderBulletLines([
|
|
230
294
|
'You may include `[[like]]` or `[[dislike]]` in a normal peer-facing reply.',
|
|
295
|
+
'You may include `[[request_conversation_end]]` in a normal peer-facing final reply when you want to formally end the conversation.',
|
|
231
296
|
'These tokens are visible to the peer.',
|
|
232
297
|
'Only the first valid feedback token for each conversation direction is recorded.',
|
|
298
|
+
'When both sides send `[[request_conversation_end]]`, the backend marks the conversation as formally ended.',
|
|
233
299
|
]),
|
|
234
300
|
]),
|
|
235
301
|
]);
|
|
236
302
|
}
|
|
237
303
|
|
|
238
|
-
function buildTaskInstructionSection({ viewer = 'recipient' } = {}) {
|
|
304
|
+
function buildTaskInstructionSection({ viewer = 'recipient', announcement = null } = {}) {
|
|
239
305
|
if (viewer !== 'sender') return null;
|
|
240
306
|
return renderSection('Task Instruction', [
|
|
241
307
|
renderBulletLines([
|
|
242
|
-
|
|
243
|
-
|
|
308
|
+
announcement
|
|
309
|
+
? 'Write one natural announcement opener to the peer now.'
|
|
310
|
+
: 'Write one natural opener to the peer now.',
|
|
311
|
+
announcement
|
|
312
|
+
? 'Make clear this is a world announcement from the owner.'
|
|
313
|
+
: 'Base it on the request brief and the background above.',
|
|
314
|
+
announcement
|
|
315
|
+
? 'Make clear the peer does not need to reply, but may reply if useful.'
|
|
316
|
+
: null,
|
|
317
|
+
announcement
|
|
318
|
+
? 'Base it on the request brief and the background above.'
|
|
319
|
+
: null,
|
|
244
320
|
'Do not quote or describe this document.',
|
|
245
321
|
'Output only the peer-facing opener.',
|
|
246
322
|
]),
|
|
@@ -281,10 +357,11 @@ export function renderAcceptedChatKickoffMarkdown(
|
|
|
281
357
|
.filter(Boolean)
|
|
282
358
|
: [];
|
|
283
359
|
const resolvedViewer = viewer === 'sender' ? 'sender' : 'recipient';
|
|
360
|
+
const announcement = resolveAnnouncementKickoff(bundle);
|
|
284
361
|
const sections = [
|
|
285
362
|
buildBackgroundSection(bundle, { viewer: resolvedViewer }),
|
|
286
363
|
buildPolicySection(bundle, { viewer: resolvedViewer }),
|
|
287
|
-
buildTaskInstructionSection({ viewer: resolvedViewer }),
|
|
364
|
+
buildTaskInstructionSection({ viewer: resolvedViewer, announcement }),
|
|
288
365
|
resolvedViewer === 'recipient'
|
|
289
366
|
? buildLiveTurnSection({ queuedTurns: normalizedQueuedTurns, includeCurrentTurnMarker: true })
|
|
290
367
|
: null,
|