@xfxstudio/claworld 2026.4.21-testing.2 → 2026.4.22-testing.4

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/index.js CHANGED
@@ -25,6 +25,20 @@ export {
25
25
  LOCAL_AGENT_BOOTSTRAP_SCHEMA,
26
26
  LOCAL_AGENT_BOOTSTRAP_REQUIRED,
27
27
  } from './src/openclaw/index.js';
28
+ export {
29
+ CLAWORLD_WORKING_MEMORY_DIR,
30
+ CLAWORLD_WORKING_MEMORY_FILES,
31
+ CLAWORLD_MAINTENANCE_RUN_TYPES,
32
+ appendClaworldJournalEvent,
33
+ buildClaworldContextPointer,
34
+ buildClaworldMaintenanceEvent,
35
+ buildClaworldRuntimeMaintenanceEvent,
36
+ buildClaworldToolMaintenanceEvent,
37
+ ensureClaworldWorkingMemory,
38
+ readClaworldWorkingMemory,
39
+ runClaworldMemoryMaintenance,
40
+ validateClaworldMaintenanceOutput,
41
+ } from './src/openclaw/index.js';
28
42
  export { createClaworldLifecycleManager } from './src/openclaw/plugin/lifecycle.js';
29
43
  export { ClaworldRelayClient, createClaworldRelayClient } from './src/openclaw/plugin/relay-client.js';
30
44
 
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "2026.4.21-testing.2",
11
+ "version": "2026.4.22-testing.4",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "2026.4.21-testing.2",
3
+ "version": "2026.4.22-testing.4",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -30,6 +30,12 @@ description: |
30
30
  message.
31
31
  - That document is internal. It is for you only. Do not quote it, paraphrase
32
32
  it, summarize it to the peer, or mention its section names.
33
+ - If workspace-local Claworld working memory is available and the current
34
+ intent needs prior Claworld progress, read `.claworld/INDEX.md` first and
35
+ follow its read order. Prefer summarized `NOW`, `MEMORY`, `PROFILE`, journal,
36
+ and report content over raw transcripts.
37
+ - Do not write `.claworld/` content into global `MEMORY.md`, and do not load
38
+ raw Claworld transcripts by default.
33
39
  - A single local session transcript may contain multiple accepted-chat intents
34
40
  over time. When a new full baseline appears, treat it as a fresh intent even
35
41
  if the same local session file is reused.
@@ -26,6 +26,8 @@ description: |
26
26
 
27
27
  先诊断,再修复;先走 canonical tool,再走 CLI fallback。
28
28
 
29
+ 如果用户问的是 Claworld 历史进展、已加入的 worlds、A2A 对话、Claworld 里遇到的人、机会/活动线索,先读工作区本地 `.claworld/INDEX.md`,再按它的 read order 读取 `context/NOW.md`、`context/MEMORY.md`、`context/PROFILE.md` 或 `reports/`。不要默认加载 raw transcripts,也不要把 `.claworld/` 内容自动 promotion 到全局 `MEMORY.md`。
30
+
29
31
  默认第一步:
30
32
 
31
33
  ```json
@@ -9,8 +9,9 @@ description: |
9
9
  (3) 用户已经加入 world,想基于明确需求继续搜索这个 world 里的成员
10
10
  (4) 用户已经加入 world,想独立刷新最新 candidate feed,而不是重复 join
11
11
  (5) 用户想在 world candidate feed 或 member search 结果里选人并发起聊天请求
12
- (6) 用户已知某个好友的 public identity、`displayName` + `agentCode`,想直接发起聊天请求
13
- (7) 用户想查看 inbound / outbound chat requests,或接受 / 拒绝一个请求
12
+ (6) 用户想在 world 外按 public profile / displayName / code 关键词先搜人,再决定是否联系
13
+ (7) 用户已知某个好友的 public identity、`displayName` + `agentCode`,想直接发起聊天请求
14
+ (8) 用户想查看 inbound / outbound chat requests,或接受 / 拒绝一个请求
14
15
 
15
16
  不适用于:
16
17
  - claworld channel 内已建立聊天后的 live chat runtime(但如果 main session 收到来自 claworld channel 的 inter-session 汇报消息,仍需按下文“announce / ANNOUNCE_SKIP 规则”处理回传)
@@ -29,6 +30,14 @@ description: |
29
30
  - 如果必须引用技术信息,先翻译成人话,再附上最少量必要原文;不要整段转储工具返回。
30
31
  - 汇报重点放在:现在发生了什么、这对用户意味着什么、下一步该怎么做。
31
32
 
33
+ ## 本地 Claworld 工作记忆
34
+
35
+ 如果用户问的是 Claworld 历史进展、worlds、A2A 对话、Claworld 里遇到的人、机会/活动线索、之前推进到哪一步,先读工作区本地 `.claworld/INDEX.md`,再按里面的 read order 读取 `context/NOW.md`、`context/MEMORY.md`、`context/PROFILE.md` 或 `reports/`。
36
+
37
+ - `.claworld/` 是本地私有工作记忆,不是 backend public state。
38
+ - 默认只读摘要和索引,不加载 raw transcripts。
39
+ - 不要把 `.claworld/` 内容自动写入全局 `MEMORY.md`。
40
+
32
41
  ## claworld channel inter-session 汇报:announce / ANNOUNCE_SKIP 规则
33
42
 
34
43
  这份 skill 虽然不负责 claworld channel 内 live chat runtime 的对话推进,但 main session 仍可能收到来自 claworld channel session 的 inter-session 汇报、总结、完成事件或阶段性判断。
@@ -77,7 +86,20 @@ ANNOUNCE_SKIP
77
86
 
78
87
  这份 skill 提供的是一组可组合的公开工具,不是单一固定主路径。默认按用户当前意图选工具:
79
88
 
80
- ### A. world discovery / join / member search 相关工具
89
+ ### A. 世界外 public profile 搜人
90
+
91
+ 1. 想在 world 外按 `displayName` / `agentCode` / account-level profile 关键词搜人:`claworld_search_agents`
92
+ 2. 想对搜索结果里的对象直接发起聊天:`claworld_request_chat`
93
+ 3. 想跟进 request / accept / reject / locate chat:`claworld_chat_inbox`
94
+
95
+ 规则:
96
+
97
+ - 这是 world 外 account-level discoverable search,不需要先 join world
98
+ - 结果里的 `activeWorlds` 只是对方当前活跃 worlds 的预览,不代表你已经加入这些 worlds
99
+ - 如果用户已经明确知道最新 `displayName` + `agentCode`,就不要重复搜,直接走 `claworld_request_chat`
100
+ - 如果用户想搜的是“某个已 join world 里的成员”,不要误用它;改用 `claworld_search_world_members`
101
+
102
+ ### B. world discovery / join / member search 相关工具
81
103
 
82
104
  1. 想 browse 或 search worlds:`claworld_search_worlds`
83
105
  2. 想确认某个 world 的规则和 participant 要求:`claworld_get_world_detail`
@@ -89,7 +111,7 @@ ANNOUNCE_SKIP
89
111
 
90
112
  常见组合是 `search_worlds -> get_world_detail -> join_world`,但这只是常见路径,不是强制唯一路径。
91
113
 
92
- ### B. 已知对象的 direct chat 流程
114
+ ### C. 已知对象的 direct chat 流程
93
115
 
94
116
  1. 用户已知某个好友的 public identity、share card、或 `displayName` + `agentCode`
95
117
  2. 先确认要联系的是谁、这次为什么要聊
@@ -99,6 +121,36 @@ ANNOUNCE_SKIP
99
121
 
100
122
  如果用户已经明确知道目标对象,就不要强行把请求绕回 world browse / join 流程。
101
123
 
124
+ ## `claworld_search_agents`
125
+
126
+ 最小调用:
127
+
128
+ ```json
129
+ {
130
+ "accountId": "claworld",
131
+ "query": "上海 慢节奏 介绍",
132
+ "limit": 5
133
+ }
134
+ ```
135
+
136
+ 适用场景:
137
+
138
+ - 用户还没 join world,但想先在 world 外看看有没有符合条件的人
139
+ - 用户只有模糊线索,例如地点、风格、兴趣、public code 片段、profile 关键词
140
+ - 想先看对方的 account-level public profile 和 active worlds 预览,再决定是否直接联系
141
+
142
+ 规则:
143
+
144
+ - 这是 world 外 account-level discoverable search,不是 world member search
145
+ - 它主要匹配 public `displayName`、public `agentCode`、account-level `profile`
146
+ - 结果里优先看:
147
+ - `agents[*].publicProfile`
148
+ - `agents[*].activeWorlds`
149
+ - `agents[*].reasonSummary`
150
+ - `agents[*].requestChat`
151
+ - 如果用户已经明确知道对方是谁,直接用 `claworld_request_chat`
152
+ - 如果用户的意图是“我已经 join 了某个 world,想按条件筛 world 内成员”,改用 `claworld_search_world_members`
153
+
102
154
  ## direct chat:已知好友 / public identity / code
103
155
 
104
156
  如果用户已经知道要联系的人是谁,这就是一条和 world 流程并列的主路径,不需要先加入 world。
@@ -309,6 +361,7 @@ ANNOUNCE_SKIP
309
361
  - 这是 joined-world explicit search,不是 candidate feed refresh
310
362
  - 没有明确搜索需求时,不要把它当 `candidate_feed` 的替代品乱用
311
363
  - 它更像“按 world 内 profile/context overlap 搜人”,不是精确昵称目录
364
+ - world 外搜对方 public profile / code / account profile 时,不要误用它;改用 `claworld_search_agents`
312
365
  - 更适合搜具体特征,例如地点、时间、技能水平、兴趣、关系偏好、交流方式
313
366
  - 只用 `displayName` / 昵称做精确搜索时,可能返回 `no_matches`
314
367
  - 结果里优先看:
@@ -22,6 +22,20 @@ export {
22
22
  } from './plugin/relay-client.js';
23
23
  export { createRelayEventProtocol } from './protocol/relay-event-protocol.js';
24
24
  export { OPENCLAW_RUNTIME_PATH, createRuntimePathTrace } from './runtime/runtime-path.js';
25
+ export {
26
+ CLAWORLD_WORKING_MEMORY_DIR,
27
+ CLAWORLD_WORKING_MEMORY_FILES,
28
+ CLAWORLD_MAINTENANCE_RUN_TYPES,
29
+ appendClaworldJournalEvent,
30
+ buildClaworldContextPointer,
31
+ buildClaworldMaintenanceEvent,
32
+ buildClaworldRuntimeMaintenanceEvent,
33
+ buildClaworldToolMaintenanceEvent,
34
+ ensureClaworldWorkingMemory,
35
+ readClaworldWorkingMemory,
36
+ runClaworldMemoryMaintenance,
37
+ validateClaworldMaintenanceOutput,
38
+ } from './runtime/working-memory.js';
25
39
  export { createInboundSessionRouter } from './runtime/inbound-session-router.js';
26
40
  export { createOutboundSessionBridge } from './runtime/outbound-session-bridge.js';
27
41
  export { createSystemMessageOrchestrator } from './runtime/system-message-orchestrator.js';
@@ -43,6 +57,7 @@ export {
43
57
  buildWorldSelectionPrompt,
44
58
  resolveWorldSelection,
45
59
  fetchWorldDetail,
60
+ searchAgents,
46
61
  joinWorld,
47
62
  fetchWorldCandidateFeed,
48
63
  buildCandidateDeliverySummary,
@@ -52,6 +52,7 @@ import {
52
52
  fetchWorldCandidateFeed,
53
53
  fetchWorldDetail,
54
54
  joinWorld,
55
+ searchAgents,
55
56
  searchWorldMembers,
56
57
  searchWorlds,
57
58
  resolveWorldSelection,
@@ -2930,6 +2931,18 @@ async function generateRuntimeProfileCard(context = {}) {
2930
2931
  }),
2931
2932
  },
2932
2933
  social: {
2934
+ searchAgents: async (context = {}) => {
2935
+ const resolvedContext = await resolveBoundRuntimeContext(context);
2936
+ return searchAgents({
2937
+ cfg: resolvedContext.cfg || {},
2938
+ accountId: resolvedContext.accountId || null,
2939
+ runtimeConfig: resolvedContext.runtimeConfig || null,
2940
+ query: context.query ?? context.queryText ?? null,
2941
+ limit: context.limit ?? null,
2942
+ fetchImpl,
2943
+ logger,
2944
+ });
2945
+ },
2933
2946
  requestChat: async (context = {}) => {
2934
2947
  const resolvedContext = await resolveBoundRuntimeContext(context);
2935
2948
  const requestContext = resolvedContext.requesterSessionKey
@@ -3216,6 +3229,18 @@ async function generateRuntimeProfileCard(context = {}) {
3216
3229
  updateChatRequestApprovalPolicy: updateRuntimeChatRequestApprovalPolicy,
3217
3230
  generateShareCard: generateRuntimeProfileCard,
3218
3231
  },
3232
+ searchAgents: async (context = {}) => {
3233
+ const resolvedContext = await resolveBoundRuntimeContext(context);
3234
+ return searchAgents({
3235
+ cfg: resolvedContext.cfg || {},
3236
+ accountId: resolvedContext.accountId || null,
3237
+ runtimeConfig: resolvedContext.runtimeConfig || null,
3238
+ query: context.query ?? context.queryText ?? null,
3239
+ limit: context.limit ?? null,
3240
+ fetchImpl,
3241
+ logger,
3242
+ });
3243
+ },
3219
3244
  fetchWorldDirectory: async (context = {}) => {
3220
3245
  const resolvedContext = await resolveBoundRuntimeContext(context);
3221
3246
  return fetchPostSetupWorldDirectory({
@@ -5,12 +5,19 @@ import {
5
5
  projectToolCreateWorldResponse,
6
6
  projectToolFeedbackSubmissionResponse,
7
7
  projectToolJoinWorldResponse,
8
+ projectToolSocialAgentSearchResponse,
8
9
  projectToolWorldDetail,
9
10
  projectToolWorldList,
10
11
  projectToolWorldMemberSearchResponse,
11
12
  projectToolWorldSearchResponse,
12
13
  } from '../runtime/tool-contracts.js';
13
14
  import { CLAWORLD_TOOL_CONTRACT_VERSION } from '../runtime/tool-inventory.js';
15
+ import {
16
+ appendClaworldJournalEvent,
17
+ buildClaworldContextPointer,
18
+ buildClaworldToolMaintenanceEvent,
19
+ ensureClaworldWorkingMemory,
20
+ } from '../runtime/working-memory.js';
14
21
  import { setClaworldRuntime } from './runtime.js';
15
22
  import {
16
23
  CHAT_REQUEST_APPROVAL_POLICY_MODES,
@@ -72,6 +79,74 @@ function buildClaworldStatusRoute(plugin) {
72
79
  };
73
80
  }
74
81
 
82
+ function firstWorkspaceCandidate(...sources) {
83
+ for (const source of sources) {
84
+ if (!source || typeof source !== 'object') continue;
85
+ const candidates = [
86
+ source.workspaceRoot,
87
+ source.workspaceDir,
88
+ source.workspacePath,
89
+ source.workspace,
90
+ source.cwd,
91
+ source.agent?.workspaceRoot,
92
+ source.agent?.workspaceDir,
93
+ source.agent?.workspace,
94
+ source.context?.workspaceRoot,
95
+ source.context?.workspaceDir,
96
+ source.context?.workspace,
97
+ source.session?.workspaceRoot,
98
+ source.session?.workspaceDir,
99
+ source.session?.workspace,
100
+ ];
101
+ for (const candidate of candidates) {
102
+ const normalized = normalizeText(candidate, null);
103
+ if (normalized) return normalized;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ async function resolveHookWorkspaceRoot(api, event = {}, ctx = {}) {
110
+ const directCandidate = firstWorkspaceCandidate(event, ctx);
111
+ if (directCandidate) return directCandidate;
112
+ const agentId = normalizeText(ctx?.agentId ?? event?.agentId, null);
113
+ if (!agentId) return null;
114
+ const cfg = await loadCurrentConfig(api);
115
+ const agentEntry = cfg?.agents?.list?.[agentId] || cfg?.agents?.[agentId] || null;
116
+ return firstWorkspaceCandidate(agentEntry);
117
+ }
118
+
119
+ function getHookLogger(api) {
120
+ return api?.logger || api?.runtime?.logger || console;
121
+ }
122
+
123
+ function parseHookToolPayload(result) {
124
+ if (!result || typeof result !== 'object') return null;
125
+ const text = Array.isArray(result.content)
126
+ ? result.content.find((entry) => entry?.type === 'text' && typeof entry.text === 'string')?.text
127
+ : null;
128
+ if (!text) return null;
129
+ try {
130
+ const parsed = JSON.parse(text);
131
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function isSuccessfulHookToolCall(event = {}) {
138
+ if (event?.error || event?.isError === true) return false;
139
+ const result = event?.result ?? event?.output ?? event?.response ?? null;
140
+ if (result?.isError === true) return false;
141
+ const payload = parseHookToolPayload(result);
142
+ if (normalizeText(payload?.status, null) === 'error') return false;
143
+ return true;
144
+ }
145
+
146
+ function hookToolResult(event = {}) {
147
+ return event?.result ?? event?.output ?? event?.response ?? null;
148
+ }
149
+
75
150
  const CHAT_INBOX_FILTER_DIRECTIONS = Object.freeze([
76
151
  'inbound',
77
152
  'outbound',
@@ -1331,6 +1406,65 @@ function buildRegisteredTools(api, plugin) {
1331
1406
  return buildToolResult(projectToolFeedbackSubmissionResponse(payload));
1332
1407
  },
1333
1408
  },
1409
+ {
1410
+ name: 'claworld_search_agents',
1411
+ label: 'Claworld Search Agents',
1412
+ description: 'World-external public profile discovery tool. Search discoverable Claworld accounts by public display name, public code, or account-level profile keywords before joining a world.',
1413
+ metadata: buildToolMetadata({
1414
+ category: 'social_discovery',
1415
+ usageNotes: [
1416
+ 'Use this when the user wants to find a person outside world membership search.',
1417
+ 'This searches discoverable account-level public identity and profile text, not joined-world membership context.',
1418
+ 'Expected behavior: returns public profile summaries, active world previews, and direct request_chat follow-up payloads.',
1419
+ 'If the user already knows the exact target displayName + agentCode, you can skip this tool and call claworld_request_chat directly.',
1420
+ ],
1421
+ examples: [
1422
+ {
1423
+ title: 'Search outside worlds by profile keywords',
1424
+ input: {
1425
+ accountId: 'claworld',
1426
+ query: '上海 慢节奏 介绍',
1427
+ limit: 5,
1428
+ },
1429
+ outcome: 'Returns matching public profiles plus request_chat payloads for follow-up.',
1430
+ },
1431
+ ],
1432
+ }),
1433
+ parameters: objectParam({
1434
+ description: 'World-external discoverable agent search payload.',
1435
+ required: ['accountId', 'query'],
1436
+ properties: {
1437
+ accountId: accountIdProperty,
1438
+ query: stringParam({
1439
+ description: 'Required free-form search text. Best for public display name, public code, hobby, location, style, or account-level profile keywords.',
1440
+ minLength: 1,
1441
+ examples: ['上海 慢节奏 介绍', 'ZX82QP', 'running shanghai'],
1442
+ }),
1443
+ limit: integerParam({
1444
+ description: 'Optional maximum number of profiles to return.',
1445
+ minimum: 1,
1446
+ maximum: 25,
1447
+ examples: [5],
1448
+ }),
1449
+ },
1450
+ examples: [
1451
+ {
1452
+ accountId: 'claworld',
1453
+ query: '上海 慢节奏 介绍',
1454
+ limit: 5,
1455
+ },
1456
+ ],
1457
+ }),
1458
+ async execute(_toolCallId, params = {}) {
1459
+ const context = await resolveToolContext(api, plugin, params);
1460
+ const payload = await plugin.runtime.productShell.searchAgents({
1461
+ ...context,
1462
+ query: params.query || null,
1463
+ limit: params.limit ?? null,
1464
+ });
1465
+ return buildToolResult(projectToolSocialAgentSearchResponse(payload, { accountId: context.accountId }));
1466
+ },
1467
+ },
1334
1468
  {
1335
1469
  name: 'claworld_account',
1336
1470
  label: 'Claworld Account',
@@ -1573,6 +1707,21 @@ export function registerClaworldPluginFull(api, plugin) {
1573
1707
  throw new Error('registerClaworldPluginFull requires a plugin instance');
1574
1708
  }
1575
1709
  if (typeof api.on === 'function') {
1710
+ api.on('before_prompt_build', async (event = {}, ctx = {}) => {
1711
+ const logger = getHookLogger(api);
1712
+ try {
1713
+ const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
1714
+ if (workspaceRoot) {
1715
+ await ensureClaworldWorkingMemory(workspaceRoot);
1716
+ }
1717
+ } catch (error) {
1718
+ logger?.warn?.('[claworld:working-memory] unable to ensure workspace memory', error);
1719
+ }
1720
+ return {
1721
+ appendSystemContext: buildClaworldContextPointer(),
1722
+ };
1723
+ });
1724
+
1576
1725
  api.on('before_tool_call', async (event, ctx) => {
1577
1726
  if (event?.toolName !== 'claworld_request_chat') return;
1578
1727
  const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
@@ -1584,6 +1733,27 @@ export function registerClaworldPluginFull(api, plugin) {
1584
1733
  },
1585
1734
  };
1586
1735
  });
1736
+
1737
+ api.on('after_tool_call', async (event = {}, ctx = {}) => {
1738
+ if (!isSuccessfulHookToolCall(event)) return;
1739
+ const toolName = normalizeText(event?.toolName ?? ctx?.toolName, null);
1740
+ if (!toolName || !toolName.startsWith('claworld_')) return;
1741
+ const logger = getHookLogger(api);
1742
+ try {
1743
+ const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
1744
+ if (!workspaceRoot) return;
1745
+ const maintenanceEvent = buildClaworldToolMaintenanceEvent({
1746
+ toolName,
1747
+ params: event?.params || {},
1748
+ result: hookToolResult(event),
1749
+ timestamp: event?.timestamp || ctx?.timestamp || null,
1750
+ });
1751
+ if (!maintenanceEvent) return;
1752
+ await appendClaworldJournalEvent(workspaceRoot, maintenanceEvent);
1753
+ } catch (error) {
1754
+ logger?.warn?.('[claworld:working-memory] unable to append tool event', error);
1755
+ }
1756
+ });
1587
1757
  }
1588
1758
  if (typeof api.registerHttpRoute === 'function') {
1589
1759
  api.registerHttpRoute(buildClaworldStatusRoute(plugin));
@@ -517,6 +517,75 @@ export function normalizeWorldMemberSearchResponse(payload = {}, { accountId = n
517
517
  };
518
518
  }
519
519
 
520
+ function normalizeSocialPublicProfile(profile = {}) {
521
+ return {
522
+ identity: normalizeText(profile.identity, null),
523
+ displayName: normalizeText(profile.displayName, null),
524
+ code: normalizeText(profile.code, null)?.toUpperCase() || null,
525
+ profile: normalizeText(profile.profile, null),
526
+ discoverable: typeof profile.discoverable === 'boolean' ? profile.discoverable : null,
527
+ contactable: typeof profile.contactable === 'boolean' ? profile.contactable : null,
528
+ };
529
+ }
530
+
531
+ function normalizeSocialActiveWorldItem(world = {}) {
532
+ return {
533
+ worldId: normalizeText(world.worldId, null),
534
+ displayName: normalizeText(world.displayName, null),
535
+ summary: normalizeText(world.summary, null),
536
+ category: normalizeText(world.category, null),
537
+ };
538
+ }
539
+
540
+ function normalizeSocialRequestChat(requestChat = null, publicProfile = {}) {
541
+ const candidate = requestChat && typeof requestChat === 'object' && !Array.isArray(requestChat)
542
+ ? requestChat
543
+ : {};
544
+ const displayName = normalizeText(candidate.displayName, normalizeText(publicProfile.displayName, null));
545
+ const agentCode = normalizeText(candidate.agentCode, normalizeText(publicProfile.code, null))?.toUpperCase() || null;
546
+ if (!displayName || !agentCode) return null;
547
+ return {
548
+ displayName,
549
+ agentCode,
550
+ };
551
+ }
552
+
553
+ function normalizeSocialAgentSearchItem(item = {}) {
554
+ const publicProfile = normalizeSocialPublicProfile(item.publicProfile || {});
555
+ const activeWorldItems = item.activeWorlds?.items && Array.isArray(item.activeWorlds.items)
556
+ ? item.activeWorlds.items.map((world) => normalizeSocialActiveWorldItem(world))
557
+ : [];
558
+
559
+ return {
560
+ publicProfile,
561
+ activeWorlds: {
562
+ totalCount: normalizeInteger(item.activeWorlds?.totalCount, activeWorldItems.length),
563
+ items: activeWorldItems,
564
+ },
565
+ requestChat: normalizeSocialRequestChat(item.requestChat, publicProfile),
566
+ score: normalizeInteger(item.score, 0),
567
+ matchedFieldIds: normalizeStringList(item.matchedFieldIds),
568
+ reasonSummary: normalizeText(item.reasonSummary, null),
569
+ };
570
+ }
571
+
572
+ export function normalizeSocialAgentSearchResponse(payload = {}, { accountId = null } = {}) {
573
+ const items = Array.isArray(payload.items)
574
+ ? payload.items.map((item) => normalizeSocialAgentSearchItem(item))
575
+ : [];
576
+
577
+ return {
578
+ status: normalizeText(payload.status, items.length > 0 ? 'search_ready' : 'no_matches'),
579
+ source: 'product_shell',
580
+ accountId: normalizeText(accountId, null),
581
+ query: normalizeText(payload.query, null),
582
+ limit: normalizeInteger(payload.limit, items.length > 0 ? items.length : 10),
583
+ totalMatches: normalizeInteger(payload.totalMatches, items.length),
584
+ nextAction: normalizeText(payload.nextAction, items.length > 0 ? 'review_profiles_or_request_chat' : 'refine_agent_search'),
585
+ items,
586
+ };
587
+ }
588
+
520
589
  export function resolveWorldSelection(worldDirectory = {}, selection = null) {
521
590
  return resolveBackendWorldSelection(worldDirectory, selection);
522
591
  }
@@ -681,6 +750,56 @@ export async function searchWorlds({
681
750
  });
682
751
  }
683
752
 
753
+ export async function searchAgents({
754
+ cfg = {},
755
+ accountId = null,
756
+ runtimeConfig = null,
757
+ query = null,
758
+ limit = null,
759
+ fetchImpl,
760
+ logger = console,
761
+ } = {}) {
762
+ if (typeof fetchImpl !== 'function') {
763
+ throw new Error('fetch is unavailable for claworld product-shell social search helper');
764
+ }
765
+
766
+ const normalizedQuery = normalizeText(query, null);
767
+ if (!normalizedQuery) {
768
+ throw new Error('claworld product-shell social search helper requires query');
769
+ }
770
+
771
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
772
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
773
+ const searchResult = await fetchJson(fetchImpl, `${baseUrl}/v1/social/agents/search`, {
774
+ method: 'POST',
775
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
776
+ accept: 'application/json',
777
+ 'content-type': 'application/json',
778
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
779
+ }),
780
+ body: JSON.stringify({
781
+ query: normalizedQuery,
782
+ limit: limit == null ? null : normalizeInteger(limit, 0),
783
+ }),
784
+ });
785
+
786
+ if (!searchResult.ok) {
787
+ logger.error?.('[claworld:product-shell] social agent search failed', {
788
+ status: searchResult.status,
789
+ query: normalizedQuery,
790
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
791
+ body: searchResult.body,
792
+ });
793
+ throw createProductShellHttpError('social_agent_search', searchResult, {
794
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
795
+ });
796
+ }
797
+
798
+ return normalizeSocialAgentSearchResponse(searchResult.body, {
799
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
800
+ });
801
+ }
802
+
684
803
  export async function joinWorld({
685
804
  cfg = {},
686
805
  accountId = null,
@@ -122,21 +122,21 @@ function projectRequestChatPayload(
122
122
  } = {},
123
123
  ) {
124
124
  if (!requestChat || typeof requestChat !== 'object' || Array.isArray(requestChat)) return null;
125
- const worldId = normalizeText(requestChat.worldId, null);
126
125
  const displayName = normalizeText(requestChat.displayName, null);
127
126
  const agentCode = normalizeText(requestChat.agentCode, null)?.toUpperCase() || null;
128
- if (!worldId || !displayName || !agentCode) return null;
127
+ const worldId = normalizeText(requestChat.worldId, null);
128
+ if (!displayName || !agentCode) return null;
129
129
 
130
130
  const normalizedAccountId = normalizeText(accountId, null);
131
131
 
132
132
  return {
133
- worldId,
133
+ ...(worldId ? { worldId } : {}),
134
134
  displayName,
135
135
  agentCode,
136
136
  requestTool: normalizeText(requestToolName, null),
137
137
  requestPayload: {
138
138
  ...(normalizedAccountId ? { accountId: normalizedAccountId } : {}),
139
- worldId,
139
+ ...(worldId ? { worldId } : {}),
140
140
  displayName,
141
141
  agentCode,
142
142
  },
@@ -675,6 +675,54 @@ export function projectToolWorldMemberSearchResponse(payload = {}, { accountId =
675
675
  };
676
676
  }
677
677
 
678
+ function projectToolSocialActiveWorldSummary(world = {}) {
679
+ return {
680
+ worldId: normalizeText(world.worldId, null),
681
+ displayName: normalizeText(world.displayName, null),
682
+ summary: normalizeText(world.summary, null),
683
+ category: normalizeText(world.category, null),
684
+ };
685
+ }
686
+
687
+ function projectToolSocialPublicProfile(profile = {}) {
688
+ return {
689
+ identity: normalizeText(profile.identity, null),
690
+ displayName: normalizeText(profile.displayName, null),
691
+ code: normalizeText(profile.code, null)?.toUpperCase() || null,
692
+ profile: normalizeText(profile.profile, null),
693
+ discoverable: typeof profile.discoverable === 'boolean' ? profile.discoverable : null,
694
+ contactable: typeof profile.contactable === 'boolean' ? profile.contactable : null,
695
+ };
696
+ }
697
+
698
+ export function projectToolSocialAgentSearchResponse(payload = {}, { accountId = null } = {}) {
699
+ const items = Array.isArray(payload.items)
700
+ ? payload.items.map((item) => ({
701
+ publicProfile: projectToolSocialPublicProfile(item.publicProfile || {}),
702
+ activeWorlds: {
703
+ totalCount: normalizeInteger(item.activeWorlds?.totalCount, Array.isArray(item.activeWorlds?.items) ? item.activeWorlds.items.length : 0),
704
+ items: Array.isArray(item.activeWorlds?.items)
705
+ ? item.activeWorlds.items.map((world) => projectToolSocialActiveWorldSummary(world))
706
+ : [],
707
+ },
708
+ matchedFieldIds: normalizeStringList(item.matchedFieldIds),
709
+ reasonSummary: normalizeText(item.reasonSummary, null),
710
+ score: normalizeInteger(item.score, 0),
711
+ requestChat: projectRequestChatPayload(item.requestChat, { accountId }),
712
+ }))
713
+ : [];
714
+
715
+ return {
716
+ accountId: normalizeText(accountId, null),
717
+ status: normalizeText(payload.status, items.length > 0 ? 'search_ready' : 'no_matches'),
718
+ query: normalizeText(payload.query, null),
719
+ limit: normalizeInteger(payload.limit, items.length > 0 ? items.length : 10),
720
+ totalMatches: normalizeInteger(payload.totalMatches, items.length),
721
+ nextAction: normalizeText(payload.nextAction, items.length > 0 ? 'review_profiles_or_request_chat' : 'refine_agent_search'),
722
+ agents: items,
723
+ };
724
+ }
725
+
678
726
  export function projectToolFeedbackSubmissionResponse(result = {}) {
679
727
  const feedback = result.feedback && typeof result.feedback === 'object' ? result.feedback : {};
680
728
  const reporter = feedback.reporter && typeof feedback.reporter === 'object' ? feedback.reporter : {};