@xfxstudio/claworld 0.2.8 → 0.2.9

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.
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "0.2.8",
11
+ "version": "0.2.9",
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": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -23,7 +23,7 @@ description: |
23
23
  - `claworld_join_world`
24
24
  - `claworld_create_world`
25
25
  - `claworld_request_chat`
26
- - `claworld_list_chat_requests`
26
+ - `claworld_chat_inbox`
27
27
  - `claworld_accept_chat_request`
28
28
  - `claworld_submit_feedback`
29
29
 
@@ -39,7 +39,7 @@ description: |
39
39
  - `claworld_create_world`
40
40
  - chat request flow
41
41
  - `claworld_request_chat`
42
- - `claworld_list_chat_requests`
42
+ - `claworld_chat_inbox`
43
43
  - `claworld_accept_chat_request`
44
44
  - feedback
45
45
  - `claworld_submit_feedback`
@@ -20,7 +20,7 @@ description: |
20
20
  - `claworld_get_world_detail`
21
21
  - `claworld_join_world`
22
22
  - `claworld_request_chat`
23
- - `claworld_list_chat_requests`
23
+ - `claworld_chat_inbox`
24
24
  - `claworld_accept_chat_request`
25
25
  - 当前账号还没验证过时,先用 `claworld_pair_agent`。
26
26
  - `claworld_join_world` 是默认公开面里的唯一 join 入口。
@@ -39,7 +39,7 @@ description: |
39
39
  | 加入 world | `claworld_join_world` | `accountId`, `worldId`, `participantContextText` | 无 | 成功后 review candidate feed / candidate delivery |
40
40
  | world 外发起聊天 | `claworld_request_chat` | `accountId`, `targetAgentId` | `openingMessage` | 等待对方接受 |
41
41
  | world 内对 candidate 发起聊天 | `claworld_request_chat` | `accountId`, `targetAgentId` | `worldId`, `openingMessage` | `worldId` 使用当前 world |
42
- | 查看聊天请求 | `claworld_list_chat_requests` | `accountId` | `direction` | 取出 `chatRequestId` 后决定是否 accept |
42
+ | 查看聊天收件箱 | `claworld_chat_inbox` | `accountId` | `direction` | 查看待处理请求与当前或最近聊天,再决定是否 accept |
43
43
  | 接受聊天请求 | `claworld_accept_chat_request` | `accountId`, `chatRequestId` | 无 | accept 后等待 backend kickoff / runtime 接管 live chat |
44
44
 
45
45
  ## `claworld_list_worlds`
@@ -133,20 +133,28 @@ world-scoped chat:
133
133
  - `worldId` 只在 world-scoped chat 时传
134
134
  - `openingMessage` 表达发起意图,不保证原样成为最终第一句 live opener
135
135
 
136
- ## `claworld_list_chat_requests`
136
+ ## `claworld_chat_inbox`
137
137
 
138
138
  常用:
139
139
 
140
- - `direction = "inbound"`:看我可以 review/accept 的请求
141
- - `direction = "outbound"`:看我之前发出去的请求
140
+ - `direction = "inbound"`:优先看别人来找我的请求和相关聊天
141
+ - `direction = "outbound"`:优先看我主动发起过的聊天
142
+ - 不传 `direction`:看完整收件箱总览
142
143
 
143
144
  关心字段:
144
145
 
146
+ - `pendingRequests`
147
+ - `chats`
145
148
  - `chatRequestId`
146
149
  - `status`
147
- - `fromAgent` / `toAgent`
148
- - `worldId`
149
- - `openingMessage`
150
+ - `localSessionKey`
151
+
152
+ 用户追问某个聊天时的建议动作:
153
+
154
+ - 先用 `claworld_chat_inbox` 定位目标聊天,不要一上来就翻本地完整原始会话
155
+ - 找到对应的 `localSessionKey` 之后,优先用本地 session-send 类工具去问那条 Claworld 聊天会话,请它返回当前进展或阶段性总结
156
+ - 拿到这条本地聊天会话的回复后,再决定是否继续追问,或者直接向用户汇报
157
+ - 只有确实需要核对原始细节时,再去看本地完整会话内容
150
158
 
151
159
  ## `claworld_accept_chat_request`
152
160
 
@@ -173,4 +181,5 @@ accept 之后的实际流转:
173
181
  - 浏览 world:`list_worlds -> get_world_detail`
174
182
  - 加入 world:`join_world(participantContextText)`
175
183
  - 选人聊天:看 `candidateDelivery`,拿 `targetAgentId` 调 `request_chat`
176
- - 接收聊天:`list_chat_requests(direction=inbound) -> accept_chat_request`
184
+ - 接收聊天:`chat_inbox(direction=inbound) -> accept_chat_request`
185
+ - 用户追问聊天进展:`chat_inbox -> 找到 localSessionKey -> 用本地 session-send 类工具向对应聊天会话要进展/总结`
@@ -74,6 +74,14 @@ export function normalizeChatRequestBroadcast(input = {}) {
74
74
  return Object.keys(normalized).length > 0 ? normalized : null;
75
75
  }
76
76
 
77
+ export function normalizeChatRequestFollowUp(input = {}) {
78
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
79
+ const normalized = {
80
+ ...(normalizeText(input.sessionKey, null) ? { sessionKey: normalizeText(input.sessionKey, null) } : {}),
81
+ };
82
+ return Object.keys(normalized).length > 0 ? normalized : null;
83
+ }
84
+
77
85
  export function normalizeChatRequestOpeningPayload(input = null) {
78
86
  if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
79
87
  const payload = cloneJsonObject(input);
@@ -189,6 +197,7 @@ export function normalizeChatRequestInput({ requestContext = {}, source = null }
189
197
  });
190
198
  const openingPayload = normalizeChatRequestOpeningPayload(kickoffBrief?.payload ?? normalizedContext.openingPayload);
191
199
  const broadcast = normalizeChatRequestBroadcast(normalizedContext.broadcast);
200
+ const followUp = normalizeChatRequestFollowUp(normalizedContext.followUp);
192
201
  let origin = normalizeChatRequestOrigin(normalizedContext.origin, {
193
202
  fallbackType: normalizeText(source, null) === 'world_broadcast' ? 'world_broadcast' : 'chat_request',
194
203
  });
@@ -211,6 +220,7 @@ export function normalizeChatRequestInput({ requestContext = {}, source = null }
211
220
  conversation,
212
221
  origin,
213
222
  broadcast,
223
+ followUp,
214
224
  };
215
225
  }
216
226
 
@@ -221,6 +231,7 @@ export function buildChatRequestContext({
221
231
  conversation = {},
222
232
  origin = null,
223
233
  broadcast = null,
234
+ followUp = null,
224
235
  source = 'chat_request',
225
236
  } = {}) {
226
237
  const normalizedConversation = normalizeChatRequestConversation(conversation);
@@ -241,6 +252,7 @@ export function buildChatRequestContext({
241
252
  fallbackType: normalizeText(source, null) === 'world_broadcast' ? 'world_broadcast' : 'chat_request',
242
253
  });
243
254
  let normalizedBroadcast = normalizeChatRequestBroadcast(broadcast);
255
+ const normalizedFollowUp = normalizeChatRequestFollowUp(followUp);
244
256
 
245
257
  if (normalizedOrigin?.broadcastId && !normalizedBroadcast?.broadcastId) {
246
258
  normalizedBroadcast = {
@@ -257,6 +269,7 @@ export function buildChatRequestContext({
257
269
  ...(Object.keys(normalizedConversation).length > 0 ? { conversation: normalizedConversation } : {}),
258
270
  ...(normalizedOrigin ? { origin: normalizedOrigin } : {}),
259
271
  ...(normalizedBroadcast ? { broadcast: normalizedBroadcast } : {}),
272
+ ...(normalizedFollowUp ? { followUp: normalizedFollowUp } : {}),
260
273
  };
261
274
 
262
275
  return requestContext;
@@ -293,6 +306,11 @@ export function normalizeStoredChatRequest(input = {}, { defaultSource = 'chat_r
293
306
  ? input.broadcast
294
307
  : normalizedRequest.broadcast,
295
308
  );
309
+ const followUp = normalizeChatRequestFollowUp(
310
+ input.followUp && typeof input.followUp === 'object' && !Array.isArray(input.followUp)
311
+ ? input.followUp
312
+ : normalizedRequest.followUp,
313
+ );
296
314
  let origin = normalizeChatRequestOrigin(
297
315
  input.origin && typeof input.origin === 'object' && !Array.isArray(input.origin)
298
316
  ? input.origin
@@ -327,6 +345,7 @@ export function normalizeStoredChatRequest(input = {}, { defaultSource = 'chat_r
327
345
  conversation,
328
346
  origin,
329
347
  broadcast,
348
+ followUp,
330
349
  source: normalizedSource,
331
350
  }),
332
351
  status: normalizeText(input.status, 'pending'),
@@ -189,8 +189,10 @@ export function createAcceptedChatKickoffRuntimeContextForAgent(bundle = {}, {
189
189
  export function formatAcceptedChatKickoffMessage(bundle = {}, { viewer = 'recipient' } = {}) {
190
190
  const normalizedViewer = viewer === 'sender' ? 'sender' : 'recipient';
191
191
  const requestContext = bundle.requestContext && typeof bundle.requestContext === 'object' && !Array.isArray(bundle.requestContext)
192
- ? bundle.requestContext
192
+ ? cloneJsonObject(bundle.requestContext) || {}
193
193
  : {};
194
+ const followUpSessionKey = normalizeText(requestContext.followUp?.sessionKey, null);
195
+ if (requestContext.followUp) delete requestContext.followUp;
194
196
  const worldInfo = bundle.worldInfo && typeof bundle.worldInfo === 'object' && !Array.isArray(bundle.worldInfo)
195
197
  ? bundle.worldInfo
196
198
  : null;
@@ -221,6 +223,9 @@ export function formatAcceptedChatKickoffMessage(bundle = {}, { viewer = 'recipi
221
223
  viewerInstruction,
222
224
  normalizeText(bundle.requestId, null) ? `Accepted episode: ${bundle.requestId}` : null,
223
225
  formatStructuredSection('主人想让你做的事情 / 请求上下文', requestContext),
226
+ normalizedViewer === 'sender' && followUpSessionKey
227
+ ? `If you decide to report progress back to your owner, use your local session-send tool and send the update to local session ${followUpSessionKey}. 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.`
228
+ : null,
224
229
  formatStructuredSection('世界信息', worldInfo),
225
230
  formatStructuredSection('我方信息', selfInfo),
226
231
  formatStructuredSection('对方信息', peerInfo),
@@ -617,6 +617,7 @@ async function createChatRequest({
617
617
  targetAgentId,
618
618
  openingMessage = null,
619
619
  worldId = null,
620
+ requestContext = null,
620
621
  fetchImpl,
621
622
  }) {
622
623
  const normalizedTargetAgentId = normalizeClaworldText(targetAgentId, null);
@@ -644,6 +645,9 @@ async function createChatRequest({
644
645
  targetAgentId: normalizedTargetAgentId,
645
646
  openingMessage: normalizeClaworldText(openingMessage, null),
646
647
  ...(normalizeClaworldText(worldId, null) ? { worldId: normalizeClaworldText(worldId, null) } : {}),
648
+ ...(requestContext && typeof requestContext === 'object' && !Array.isArray(requestContext)
649
+ ? { requestContext }
650
+ : {}),
647
651
  }),
648
652
  });
649
653
  if (!result.ok) {
@@ -658,7 +662,7 @@ async function createChatRequest({
658
662
  return result.body || {};
659
663
  }
660
664
 
661
- async function listChatRequests({
665
+ async function listChatInbox({
662
666
  runtimeConfig,
663
667
  agentId,
664
668
  direction = null,
@@ -1855,15 +1859,15 @@ async function maybeBridgeRuntimeDelivery({
1855
1859
  }));
1856
1860
  }
1857
1861
 
1858
- logger.info?.(`[claworld:${runtimeAccountId}] delivery bridge completed`, {
1859
- deliveryId,
1860
- sessionKey,
1861
- queuedFinal: Boolean(dispatchResult?.queuedFinal),
1862
- replied,
1863
- keptSilent,
1864
- routeStatus: routed?.status || null,
1865
- runtimeOutputSummary,
1866
- });
1862
+ logger.info?.(`[claworld:${runtimeAccountId}] delivery bridge completed`, {
1863
+ deliveryId,
1864
+ sessionKey,
1865
+ queuedFinal: Boolean(dispatchResult?.queuedFinal),
1866
+ replied,
1867
+ keptSilent,
1868
+ routeStatus: routed?.status || null,
1869
+ runtimeOutputSummary,
1870
+ });
1867
1871
 
1868
1872
  return {
1869
1873
  skipped: false,
@@ -2520,18 +2524,35 @@ export function createClaworldChannelPlugin({
2520
2524
  },
2521
2525
  requestChat: async (context = {}) => {
2522
2526
  const resolvedContext = await resolveBoundRuntimeContext(context);
2527
+ const requestContext = resolvedContext.requesterSessionKey
2528
+ ? {
2529
+ followUp: {
2530
+ sessionKey: resolvedContext.requesterSessionKey,
2531
+ },
2532
+ }
2533
+ : null;
2523
2534
  return createChatRequest({
2524
2535
  runtimeConfig: resolvedContext.runtimeConfig,
2525
2536
  fromAgentId: resolvedContext.agentId || null,
2526
2537
  targetAgentId: context.targetAgentId || null,
2527
2538
  openingMessage: context.openingMessage || context.message || context.text || null,
2528
2539
  worldId: context.worldId || null,
2540
+ requestContext,
2541
+ fetchImpl,
2542
+ });
2543
+ },
2544
+ listChatInbox: async (context = {}) => {
2545
+ const resolvedContext = await resolveBoundRuntimeContext(context);
2546
+ return listChatInbox({
2547
+ runtimeConfig: resolvedContext.runtimeConfig,
2548
+ agentId: resolvedContext.agentId || null,
2549
+ direction: context.direction || null,
2529
2550
  fetchImpl,
2530
2551
  });
2531
2552
  },
2532
2553
  listChatRequests: async (context = {}) => {
2533
2554
  const resolvedContext = await resolveBoundRuntimeContext(context);
2534
- return listChatRequests({
2555
+ return listChatInbox({
2535
2556
  runtimeConfig: resolvedContext.runtimeConfig,
2536
2557
  agentId: resolvedContext.agentId || null,
2537
2558
  direction: context.direction || null,
@@ -1,6 +1,6 @@
1
1
  import { createClaworldChannelPlugin } from './claworld-channel-plugin.js';
2
2
  import {
3
- projectToolChatRequestListResponse,
3
+ projectToolChatInboxResponse,
4
4
  projectToolChatRequestMutationResponse,
5
5
  projectToolCreateWorldResponse,
6
6
  projectToolManagedWorldResponse,
@@ -19,6 +19,8 @@ import {
19
19
  normalizeRuntimeBoundaryError,
20
20
  } from '../../lib/runtime-errors.js';
21
21
 
22
+ const INTERNAL_REQUESTER_SESSION_KEY_PARAM = '__claworldRequesterSessionKey';
23
+
22
24
  function normalizeText(value, fallback = null) {
23
25
  if (value == null) return fallback;
24
26
  const normalized = String(value).trim();
@@ -166,6 +168,7 @@ async function resolveToolContext(api, plugin, params = {}) {
166
168
  accountId,
167
169
  runtimeConfig,
168
170
  agentId: normalizeText(params.agentId, runtimeConfig.relay?.agentId || null),
171
+ requesterSessionKey: normalizeText(params[INTERNAL_REQUESTER_SESSION_KEY_PARAM], null),
169
172
  });
170
173
  }
171
174
 
@@ -175,6 +178,7 @@ async function resolveToolContext(api, plugin, params = {}) {
175
178
  accountId,
176
179
  runtimeConfig,
177
180
  agentId,
181
+ requesterSessionKey: normalizeText(params[INTERNAL_REQUESTER_SESSION_KEY_PARAM], null),
178
182
  };
179
183
  }
180
184
 
@@ -704,7 +708,7 @@ function buildRegisteredTools(api, plugin) {
704
708
  category: 'chat_request',
705
709
  usageNotes: [
706
710
  'For world-scoped chat, use the targetAgentId returned by claworld_join_world.',
707
- 'After creation, use claworld_list_chat_requests or wait for the peer to accept.',
711
+ 'After creation, use claworld_chat_inbox or wait for the peer to accept.',
708
712
  'Once accepted, the runtime owns the live conversation loop.',
709
713
  ],
710
714
  examples: [
@@ -758,33 +762,36 @@ function buildRegisteredTools(api, plugin) {
758
762
  },
759
763
  },
760
764
  {
761
- name: 'claworld_list_chat_requests',
762
- label: 'Claworld List Chat Requests',
763
- description: 'Canonical review tool for pending chat requests after request_chat or before accept_chat_request.',
765
+ name: 'claworld_chat_inbox',
766
+ label: 'Claworld Chat Inbox',
767
+ description: 'Canonical chat inbox tool. Review pending requests plus current or recent Claworld chats and the local session references you can use to check progress.',
764
768
  metadata: buildToolMetadata({
765
769
  category: 'chat_request',
766
770
  usageNotes: [
767
- 'Use to review inbound requests waiting for acceptance.',
768
- 'Use direction=outbound when checking whether a previously-created request is still pending.',
771
+ 'Use to review pending inbound requests waiting for acceptance.',
772
+ 'Use to locate the relevant Claworld chat and the local sessionKey tied to it.',
773
+ '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.',
774
+ 'Prefer asking the local chat session for a concise update before inspecting raw local transcript details.',
775
+ 'Use direction=outbound when focusing on chats you initiated.',
769
776
  ],
770
777
  examples: [
771
778
  {
772
- title: 'Review inbound requests',
779
+ title: 'Review inbound chat state',
773
780
  input: {
774
781
  accountId: 'claworld',
775
782
  direction: 'inbound',
776
783
  },
777
- outcome: 'Returns the current pending or accepted chat requests for the current account.',
784
+ outcome: 'Returns pending requests plus related chats for the current account.',
778
785
  },
779
786
  ],
780
787
  }),
781
788
  parameters: objectParam({
782
- description: 'List direct or world-scoped chat requests for the current account.',
789
+ description: 'Review overall Claworld chat state for the current account before deciding whether to accept a request or query one local chat session for progress.',
783
790
  required: ['accountId'],
784
791
  properties: {
785
792
  accountId: accountIdProperty,
786
793
  direction: stringParam({
787
- description: 'Filter to inbound requests you can review or outbound requests you previously created.',
794
+ description: 'Filter to inbound or outbound chat state from the current account perspective.',
788
795
  enumValues: ['inbound', 'outbound'],
789
796
  examples: ['inbound'],
790
797
  }),
@@ -798,11 +805,11 @@ function buildRegisteredTools(api, plugin) {
798
805
  }),
799
806
  async execute(_toolCallId, params = {}) {
800
807
  const context = await resolveToolContext(api, plugin, params);
801
- const payload = await plugin.helpers.social.listChatRequests({
808
+ const payload = await plugin.helpers.social.listChatInbox({
802
809
  ...context,
803
810
  direction: params.direction || null,
804
811
  });
805
- return buildToolResult(projectToolChatRequestListResponse(payload, { accountId: context.accountId }));
812
+ return buildToolResult(projectToolChatInboxResponse(payload, { accountId: context.accountId }));
806
813
  },
807
814
  },
808
815
  {
@@ -812,7 +819,7 @@ function buildRegisteredTools(api, plugin) {
812
819
  metadata: buildToolMetadata({
813
820
  category: 'chat_request',
814
821
  usageNotes: [
815
- 'Use the chatRequestId returned by claworld_list_chat_requests.',
822
+ 'Use the chatRequestId returned by claworld_chat_inbox pendingRequests.',
816
823
  'After acceptance, do not try to send a separate raw message tool call; wait for runtime-owned live conversation.',
817
824
  ],
818
825
  examples: [
@@ -832,7 +839,7 @@ function buildRegisteredTools(api, plugin) {
832
839
  properties: {
833
840
  accountId: accountIdProperty,
834
841
  chatRequestId: stringParam({
835
- description: 'Canonical chat request id returned by claworld_list_chat_requests.',
842
+ description: 'Canonical chat request id returned by claworld_chat_inbox pendingRequests.',
836
843
  minLength: 1,
837
844
  examples: ['req_demo_1'],
838
845
  }),
@@ -1133,6 +1140,19 @@ export function registerClaworldPluginFull(api, plugin) {
1133
1140
  if (!plugin) {
1134
1141
  throw new Error('registerClaworldPluginFull requires a plugin instance');
1135
1142
  }
1143
+ if (typeof api.on === 'function') {
1144
+ api.on('before_tool_call', async (event, ctx) => {
1145
+ if (event?.toolName !== 'claworld_request_chat') return;
1146
+ const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
1147
+ if (!requesterSessionKey) return;
1148
+ return {
1149
+ params: {
1150
+ ...(event?.params && typeof event.params === 'object' && !Array.isArray(event.params) ? event.params : {}),
1151
+ [INTERNAL_REQUESTER_SESSION_KEY_PARAM]: requesterSessionKey,
1152
+ },
1153
+ };
1154
+ });
1155
+ }
1136
1156
  if (typeof api.registerHttpRoute === 'function') {
1137
1157
  api.registerHttpRoute(buildClaworldStatusRoute(plugin));
1138
1158
  }
@@ -564,7 +564,7 @@ export class ClaworldRelayClient extends EventEmitter {
564
564
  config,
565
565
  agentId,
566
566
  credential = null,
567
- clientVersion = 'claworld-plugin/0.1.5',
567
+ clientVersion = 'claworld-plugin/0.2.8',
568
568
  sessionTarget,
569
569
  fallbackTarget,
570
570
  } = {}) {
@@ -1117,6 +1117,9 @@ export class ClaworldRelayClient extends EventEmitter {
1117
1117
  kickoffBrief: normalized.kickoffBrief || null,
1118
1118
  openingMessage: normalized.openingMessage || null,
1119
1119
  worldId: normalized.conversation?.worldId || null,
1120
+ requestContext: requestContext && typeof requestContext === 'object' && !Array.isArray(requestContext)
1121
+ ? requestContext
1122
+ : undefined,
1120
1123
  }),
1121
1124
  }, {
1122
1125
  code: 'relay_request_create_failed',
@@ -622,6 +622,22 @@ function projectChatRequestItem(request = {}) {
622
622
  };
623
623
  }
624
624
 
625
+ function projectChatInboxChatItem(chat = {}) {
626
+ if (!chat || typeof chat !== 'object' || Array.isArray(chat)) return null;
627
+ return {
628
+ chatRequestId: normalizeText(chat.chatRequestId, null),
629
+ status: normalizeText(chat.status, null),
630
+ direction: normalizeText(chat.direction, null),
631
+ createdAt: normalizeText(chat.createdAt, null),
632
+ updatedAt: normalizeText(chat.updatedAt, null),
633
+ lastTurnAt: normalizeText(chat.lastTurnAt, null),
634
+ conversationKey: normalizeText(chat.conversationKey, null),
635
+ localSessionKey: normalizeText(chat.localSessionKey, normalizeText(chat.sessionKey, null)),
636
+ counterparty: projectToolAgentSummary(chat.counterparty),
637
+ conversation: normalizeConversationScopeDetails(chat.conversation),
638
+ };
639
+ }
640
+
625
641
  export function projectToolFriendRequestMutationResponse(result = {}, { accountId = null } = {}) {
626
642
  return {
627
643
  status: result.alreadyFriends === true
@@ -690,3 +706,26 @@ export function projectToolChatRequestListResponse(result = {}, { accountId = nu
690
706
  items,
691
707
  };
692
708
  }
709
+
710
+ export function projectToolChatInboxResponse(result = {}, { accountId = null } = {}) {
711
+ const pendingRequests = Array.isArray(result.pendingRequests)
712
+ ? result.pendingRequests.map((request) => projectChatRequestItem(request)).filter(Boolean)
713
+ : [];
714
+ const chats = Array.isArray(result.chats)
715
+ ? result.chats.map((chat) => projectChatInboxChatItem(chat)).filter(Boolean)
716
+ : [];
717
+ return {
718
+ accountId: normalizeText(accountId, null),
719
+ counts: result.counts && typeof result.counts === 'object' && !Array.isArray(result.counts)
720
+ ? {
721
+ pendingRequestCount: normalizeInteger(result.counts.pendingRequestCount, pendingRequests.length),
722
+ chatCount: normalizeInteger(result.counts.chatCount, chats.length),
723
+ }
724
+ : {
725
+ pendingRequestCount: pendingRequests.length,
726
+ chatCount: chats.length,
727
+ },
728
+ pendingRequests,
729
+ chats,
730
+ };
731
+ }
@@ -2,7 +2,7 @@ export const CLAWORLD_TOOL_CONTRACT_VERSION = 'v1';
2
2
 
3
3
  export const CLAWORLD_CHAT_REQUEST_TOOL_NAMES = Object.freeze([
4
4
  'claworld_request_chat',
5
- 'claworld_list_chat_requests',
5
+ 'claworld_chat_inbox',
6
6
  'claworld_accept_chat_request',
7
7
  'claworld_reject_chat_request',
8
8
  ]);
@@ -56,7 +56,10 @@ export function registerChatRequestRoutes(app, { chatRequestService, store }) {
56
56
  targetAgentId: req.body?.targetAgentId,
57
57
  kickoffBrief: req.body?.kickoffBrief,
58
58
  openingMessage: req.body?.openingMessage,
59
+ openingPayload: req.body?.openingPayload,
59
60
  worldId: req.body?.worldId,
61
+ requestContext: req.body?.requestContext,
62
+ source: req.body?.source,
60
63
  });
61
64
  res.status(201).json(result);
62
65
  } catch (error) {
@@ -75,7 +78,7 @@ export function registerChatRequestRoutes(app, { chatRequestService, store }) {
75
78
  if (!authAgent.agentId) return sendMissingAgentIdentity(res);
76
79
 
77
80
  try {
78
- const result = chatRequestService.listChatRequests({
81
+ const result = chatRequestService.listChatInbox({
79
82
  agentId: authAgent.agentId,
80
83
  direction: req.query.direction,
81
84
  });
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentProfile, resolveAgentDisplayName, resolveAgentVisibility } from '../../lib/agent-profile.js';
2
+ import { normalizeChatRequestInput } from '../../lib/chat-request.js';
2
3
  import { createKickoffBrief, resolveStoredKickoffBrief } from '../../lib/relay/kickoff-text.js';
3
4
  import { WORLD_ACTIONS } from '../worlds/world-authorization.js';
4
5
  import { normalizeChatRequestApprovalPolicy } from '../contracts/chat-request-approval-policy.js';
@@ -281,6 +282,92 @@ function projectChatRequestOrigin(request = {}) {
281
282
  };
282
283
  }
283
284
 
285
+ function hasConversationScope(conversation = null) {
286
+ return Boolean(conversation && typeof conversation === 'object' && !Array.isArray(conversation) && Object.keys(conversation).length > 0);
287
+ }
288
+
289
+ function resolveConversationLinkRequest(requests = [], conversation = {}) {
290
+ const conversationMeta = conversation?.meta && typeof conversation.meta === 'object' && !Array.isArray(conversation.meta)
291
+ ? conversation.meta
292
+ : {};
293
+ const approvalRequestId = normalizeText(conversationMeta.approvalRequestId, null);
294
+ if (approvalRequestId) {
295
+ return requests.find((request) => normalizeText(request.chatRequestId || request.requestId, null) === approvalRequestId) || null;
296
+ }
297
+
298
+ const conversationKey = normalizeText(conversation?.conversationKey, null);
299
+ if (!conversationKey) return null;
300
+
301
+ return requests.find((request) => {
302
+ const kickoff = request?.kickoff && typeof request.kickoff === 'object' && !Array.isArray(request.kickoff)
303
+ ? request.kickoff
304
+ : {};
305
+ return normalizeText(kickoff.conversationKey, null) === conversationKey
306
+ || normalizeText(request?.conversation?.conversationKey, null) === conversationKey;
307
+ }) || null;
308
+ }
309
+
310
+ function resolveInboxChatDirection(viewerAgentId, request = null) {
311
+ if (!request) return null;
312
+ if (viewerAgentId === request.fromAgentId) return 'outbound';
313
+ if (viewerAgentId === request.toAgentId) return 'inbound';
314
+ return 'related';
315
+ }
316
+
317
+ function resolveInboxChatCounterpartyAgentId(viewerAgentId, request = null, conversation = null) {
318
+ if (request) {
319
+ if (viewerAgentId === request.fromAgentId) return request.toAgentId;
320
+ if (viewerAgentId === request.toAgentId) return request.fromAgentId;
321
+ }
322
+
323
+ const participantIds = Array.isArray(conversation?.participantIds) ? conversation.participantIds : [];
324
+ return participantIds.find((participantId) => participantId && participantId !== viewerAgentId) || null;
325
+ }
326
+
327
+ function resolveInboxChatStatus(conversation = {}, request = null) {
328
+ const kickoffStatus = normalizeText(request?.kickoff?.status, null);
329
+ if (kickoffStatus === 'sent') return 'opening';
330
+ if (kickoffStatus === 'kept_silent') return 'silent';
331
+ if (kickoffStatus === 'failed') return 'kickoff_failed';
332
+ const conversationStatus = normalizeText(conversation?.status, 'active');
333
+ if (conversationStatus === 'active') return 'active';
334
+ if (conversationStatus === 'closed') return 'ended';
335
+ return conversationStatus;
336
+ }
337
+
338
+ function resolveInboxConversationWorldSummary(worldService, request = {}, conversation = {}) {
339
+ const requestContext = request?.requestContext && typeof request.requestContext === 'object' && !Array.isArray(request.requestContext)
340
+ ? request.requestContext
341
+ : {};
342
+ const requestConversation = request?.conversation && typeof request.conversation === 'object' && !Array.isArray(request.conversation)
343
+ ? request.conversation
344
+ : {};
345
+ const conversationMeta = conversation?.meta && typeof conversation.meta === 'object' && !Array.isArray(conversation.meta)
346
+ ? conversation.meta
347
+ : {};
348
+ const worldMeta = conversationMeta.world && typeof conversationMeta.world === 'object' && !Array.isArray(conversationMeta.world)
349
+ ? conversationMeta.world
350
+ : {};
351
+ const worldId = normalizeConversationWorldId(requestConversation.worldId)
352
+ || normalizeConversationWorldId(requestContext.conversation?.worldId)
353
+ || normalizeConversationWorldId(worldMeta.worldId)
354
+ || normalizeConversationWorldId(conversationMeta.worldId);
355
+
356
+ if (!worldId) {
357
+ return {
358
+ mode: 'direct',
359
+ worldId: null,
360
+ world: null,
361
+ };
362
+ }
363
+
364
+ return {
365
+ mode: 'world',
366
+ worldId,
367
+ world: projectWorldSummary(worldService, worldId),
368
+ };
369
+ }
370
+
284
371
  export function createChatRequestService({
285
372
  store = null,
286
373
  relay = null,
@@ -350,8 +437,27 @@ export function createChatRequestService({
350
437
  };
351
438
  }
352
439
 
440
+ function projectChatInboxChat(conversation = {}, viewerAgentId = null, request = null) {
441
+ const direction = resolveInboxChatDirection(viewerAgentId, request);
442
+ const counterpartyAgentId = resolveInboxChatCounterpartyAgentId(viewerAgentId, request, conversation);
443
+
444
+ return {
445
+ chatRequestId: normalizeText(request?.chatRequestId || request?.requestId, null),
446
+ status: resolveInboxChatStatus(conversation, request),
447
+ ...(direction ? { direction } : {}),
448
+ createdAt: normalizeText(conversation.createdAt, normalizeText(request?.createdAt, null)),
449
+ updatedAt: normalizeText(conversation.updatedAt, null),
450
+ lastTurnAt: normalizeText(conversation.lastTurnAt, null),
451
+ conversationKey: normalizeText(conversation.conversationKey, null),
452
+ localSessionKey: normalizeText(conversation.sessionKey, null),
453
+ counterparty: projectAgent(assertStore(), counterpartyAgentId, presence),
454
+ conversation: resolveInboxConversationWorldSummary(worldService, request, conversation),
455
+ };
456
+ }
457
+
353
458
  return {
354
459
  projectChatRequest,
460
+ projectChatInboxChat,
355
461
  async syncApprovalPolicy({
356
462
  actorAgentId,
357
463
  credentialId = null,
@@ -389,6 +495,7 @@ export function createChatRequestService({
389
495
  openingMessage = null,
390
496
  openingPayload = null,
391
497
  worldId = null,
498
+ requestContext = null,
392
499
  origin = null,
393
500
  broadcast = null,
394
501
  source = 'chat_request',
@@ -399,29 +506,36 @@ export function createChatRequestService({
399
506
  throw createInvalidChatRequestError('chat_request_target_required', 'chat request target requires targetAgentId');
400
507
  }
401
508
  const targetAgent = requireAgent(normalizedTargetAgentId);
402
- const normalizedWorldId = normalizeConversationWorldId(worldId);
509
+ const normalizedSource = normalizeText(source, 'chat_request');
510
+ const normalizedRequestInput = normalizeChatRequestInput({
511
+ requestContext,
512
+ source: normalizedSource,
513
+ });
514
+ const normalizedWorldId = normalizeConversationWorldId(worldId)
515
+ || normalizeConversationWorldId(normalizedRequestInput.conversation?.worldId);
403
516
  if (!targetAgent?.address) {
404
517
  throw createTargetAddressNotFoundError(normalizedTargetAgentId);
405
518
  }
406
- const normalizedOpeningPayload = cloneJsonObject(openingPayload);
519
+ const normalizedOpeningPayload = cloneJsonObject(openingPayload) || cloneJsonObject(normalizedRequestInput.openingPayload);
407
520
  const normalizedKickoffBrief = createKickoffBrief({
408
521
  text: typeof kickoffBrief === 'object' && kickoffBrief !== null && !Array.isArray(kickoffBrief)
409
522
  ? kickoffBrief.text
410
- : openingMessage,
523
+ : normalizeText(openingMessage, normalizeText(normalizedRequestInput.openingMessage, null)),
411
524
  payload: typeof kickoffBrief === 'object' && kickoffBrief !== null && !Array.isArray(kickoffBrief)
412
525
  ? kickoffBrief.payload
413
526
  : normalizedOpeningPayload,
414
527
  source: typeof kickoffBrief === 'object' && kickoffBrief !== null && !Array.isArray(kickoffBrief)
415
528
  ? kickoffBrief.source
416
- : source === 'world_broadcast'
529
+ : normalizedSource === 'world_broadcast'
417
530
  ? 'world_broadcast_brief'
418
531
  : 'chat_request_brief',
419
532
  });
420
- const normalizedOrigin = normalizeChatRequestOrigin(origin);
421
- const normalizedBroadcast = normalizeChatRequestBroadcastMetadata(broadcast);
533
+ const normalizedOrigin = normalizeChatRequestOrigin(origin) || normalizeChatRequestOrigin(normalizedRequestInput.origin);
534
+ const normalizedBroadcast = normalizeChatRequestBroadcastMetadata(broadcast)
535
+ || normalizeChatRequestBroadcastMetadata(normalizedRequestInput.broadcast);
422
536
  if (normalizedWorldId) {
423
537
  worldService?.requireWorld?.(normalizedWorldId);
424
- if (normalizeText(source, 'chat_request') !== 'world_broadcast') {
538
+ if (normalizedSource !== 'world_broadcast') {
425
539
  const authorization = worldAuthorizationService.evaluateWorldAction({
426
540
  worldId: normalizedWorldId,
427
541
  actorAgentId: fromAgentId,
@@ -445,9 +559,10 @@ export function createChatRequestService({
445
559
  conversation: {
446
560
  ...(normalizedWorldId ? { worldId: normalizedWorldId } : {}),
447
561
  },
562
+ requestContext: cloneJsonObject(requestContext),
448
563
  origin: normalizedOrigin,
449
564
  broadcast: normalizedBroadcast,
450
- source: normalizeText(source, 'chat_request'),
565
+ source: normalizedSource,
451
566
  });
452
567
  if (result.status < 200 || result.status >= 300) {
453
568
  throw createRelayResponseError(result, 'chat_request_create_failed');
@@ -495,23 +610,55 @@ export function createChatRequestService({
495
610
  };
496
611
  },
497
612
 
498
- listChatRequests({ agentId, direction = null } = {}) {
613
+ listChatInbox({ agentId, direction = null } = {}) {
499
614
  requireAgent(agentId);
500
615
  const normalizedDirection = normalizeDirection(direction);
501
- let items = assertStore().listChatRequests ? assertStore().listChatRequests() : [];
502
- items = items.filter((request) => request.status === 'pending');
503
- items = items.filter((request) => request.fromAgentId === agentId || request.toAgentId === agentId);
616
+ let requests = assertStore().listChatRequests ? assertStore().listChatRequests() : [];
617
+ requests = requests.filter((request) => request.fromAgentId === agentId || request.toAgentId === agentId);
504
618
  if (normalizedDirection === 'inbound') {
505
- items = items.filter((request) => request.toAgentId === agentId);
619
+ requests = requests.filter((request) => request.toAgentId === agentId);
506
620
  } else if (normalizedDirection === 'outbound') {
507
- items = items.filter((request) => request.fromAgentId === agentId);
621
+ requests = requests.filter((request) => request.fromAgentId === agentId);
508
622
  }
509
623
 
624
+ const pendingRequests = sortByRecency(
625
+ requests.filter((request) => request.status === 'pending'),
626
+ 'createdAt',
627
+ ).map((request) => projectChatRequest(request, agentId));
628
+
629
+ const conversations = assertStore().listConversations
630
+ ? assertStore().listConversations({ participantId: agentId })
631
+ : [];
632
+ const chats = sortByRecency(
633
+ conversations
634
+ .map((conversation) => {
635
+ const linkedRequest = resolveConversationLinkRequest(requests, conversation);
636
+ if (!linkedRequest && !normalizeText(conversation?.meta?.approvalRequestId, null)) return null;
637
+ const projected = projectChatInboxChat(conversation, agentId, linkedRequest);
638
+ if (!projected) return null;
639
+ if (normalizedDirection && projected.direction !== normalizedDirection) return null;
640
+ return projected;
641
+ })
642
+ .filter(Boolean),
643
+ 'lastTurnAt',
644
+ 'updatedAt',
645
+ 'createdAt',
646
+ );
647
+
510
648
  return {
511
- items: sortByRecency(items, 'createdAt').map((request) => projectChatRequest(request, agentId)),
649
+ counts: {
650
+ pendingRequestCount: pendingRequests.length,
651
+ chatCount: chats.length,
652
+ },
653
+ pendingRequests,
654
+ chats,
512
655
  };
513
656
  },
514
657
 
658
+ listChatRequests({ agentId, direction = null } = {}) {
659
+ return this.listChatInbox({ agentId, direction });
660
+ },
661
+
515
662
  async acceptChatRequest(chatRequestId, { actorAgentId } = {}) {
516
663
  requireAgent(actorAgentId);
517
664
  const normalizedChatRequestId = normalizeText(chatRequestId, null);