@xfxstudio/claworld 2026.4.16-testing.1 → 2026.4.16-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.
Files changed (48) hide show
  1. package/README.md +3 -3
  2. package/index.js +50 -0
  3. package/openclaw.plugin.json +1 -1
  4. package/package.json +4 -1
  5. package/setup-entry.js +6 -0
  6. package/skills/claworld-a2a-channel-agent/SKILL.md +218 -0
  7. package/skills/claworld-help/SKILL.md +304 -0
  8. package/skills/claworld-join-and-chat/SKILL.md +526 -0
  9. package/skills/claworld-manage-worlds/SKILL.md +292 -0
  10. package/skills/claworld-manage-worlds/references/world-context-templates.md +145 -0
  11. package/src/lib/chat-request.js +366 -0
  12. package/src/lib/public-identity.js +175 -0
  13. package/src/lib/relay/agent-readable-markdown.js +385 -0
  14. package/src/lib/relay/kickoff-progress.js +162 -0
  15. package/src/lib/relay/kickoff-text.js +191 -0
  16. package/src/lib/relay/shared.js +30 -0
  17. package/src/lib/runtime-errors.js +149 -0
  18. package/src/openclaw/index.js +51 -0
  19. package/src/openclaw/plugin/account-identity.js +73 -0
  20. package/src/openclaw/plugin/claworld-channel-plugin.js +3499 -0
  21. package/src/openclaw/plugin/config-schema.js +392 -0
  22. package/src/openclaw/plugin/lifecycle.js +114 -0
  23. package/src/openclaw/plugin/managed-config.js +1054 -0
  24. package/src/openclaw/plugin/onboarding.js +312 -0
  25. package/src/openclaw/plugin/register-tooling.js +728 -0
  26. package/src/openclaw/plugin/register.js +1616 -0
  27. package/src/openclaw/plugin/relay-client-shared.js +146 -0
  28. package/src/openclaw/plugin/relay-client.js +1469 -0
  29. package/src/openclaw/plugin/runtime-backup.js +105 -0
  30. package/src/openclaw/plugin/runtime.js +12 -0
  31. package/src/openclaw/plugin-version.js +67 -0
  32. package/src/openclaw/protocol/relay-event-protocol.js +43 -0
  33. package/src/openclaw/runtime/backend-error-context.js +91 -0
  34. package/src/openclaw/runtime/canonical-result-builder.js +126 -0
  35. package/src/openclaw/runtime/demo-session-bootstrap.js +32 -0
  36. package/src/openclaw/runtime/feedback-helper.js +145 -0
  37. package/src/openclaw/runtime/inbound-session-router.js +44 -0
  38. package/src/openclaw/runtime/outbound-session-bridge.js +29 -0
  39. package/src/openclaw/runtime/product-shell-helper.js +931 -0
  40. package/src/openclaw/runtime/runtime-path.js +19 -0
  41. package/src/openclaw/runtime/system-message-orchestrator.js +1 -0
  42. package/src/openclaw/runtime/tool-contracts.js +939 -0
  43. package/src/openclaw/runtime/tool-inventory.js +83 -0
  44. package/src/openclaw/runtime/world-membership-helper.js +320 -0
  45. package/src/openclaw/runtime/world-moderation-helper.js +508 -0
  46. package/src/product-shell/contracts/chat-request-approval-policy.js +93 -0
  47. package/src/product-shell/contracts/world-orchestration.js +734 -0
  48. package/src/product-shell/orchestration/world-conversation-text.js +229 -0
@@ -0,0 +1,1616 @@
1
+ import { createClaworldChannelPlugin } from './claworld-channel-plugin.js';
2
+ import {
3
+ projectToolChatRequestMutationResponse,
4
+ projectToolCandidateFeedResponse,
5
+ projectToolCreateWorldResponse,
6
+ projectToolFeedbackSubmissionResponse,
7
+ projectToolJoinWorldResponse,
8
+ projectToolWorldDetail,
9
+ projectToolWorldList,
10
+ projectToolWorldMemberSearchResponse,
11
+ projectToolWorldSearchResponse,
12
+ } from '../runtime/tool-contracts.js';
13
+ import { CLAWORLD_TOOL_CONTRACT_VERSION } from '../runtime/tool-inventory.js';
14
+ import { setClaworldRuntime } from './runtime.js';
15
+ import {
16
+ CHAT_REQUEST_APPROVAL_POLICY_MODES,
17
+ CHAT_REQUEST_APPROVAL_POLICY_ORIGIN_TYPES,
18
+ } from '../../product-shell/contracts/chat-request-approval-policy.js';
19
+ import {
20
+ ACCOUNT_ACTIONS,
21
+ arrayParam,
22
+ booleanParam,
23
+ buildToolMetadata,
24
+ buildToolResult,
25
+ CHAT_INBOX_ACTIONS,
26
+ inferAccountAction,
27
+ inferChatInboxAction,
28
+ inferManageWorldAction,
29
+ INTERNAL_REQUESTER_SESSION_KEY_PARAM,
30
+ integerParam,
31
+ loadCurrentConfig,
32
+ MANAGE_WORLD_ACTIONS,
33
+ normalizeManageWorldAction,
34
+ normalizeObject,
35
+ normalizeText,
36
+ objectParam,
37
+ projectToolAccountMutationResponse,
38
+ projectToolAccountViewResponse,
39
+ projectToolChatInboxActionResponse,
40
+ projectToolManageWorldActionResponse,
41
+ requireManageWorldField,
42
+ resolveToolContext,
43
+ stringParam,
44
+ withToolErrorBoundary,
45
+ } from './register-tooling.js';
46
+
47
+ function buildClaworldStatusRoute(plugin) {
48
+ return {
49
+ method: 'GET',
50
+ path: '/plugins/claworld/status',
51
+ auth: 'gateway',
52
+ match: 'exact',
53
+ async handler(_req, res) {
54
+ const payload = plugin.status?.getSnapshot?.() || {
55
+ ok: true,
56
+ pluginId: plugin.id || 'claworld',
57
+ };
58
+ if (typeof res?.status === 'function' && typeof res?.json === 'function') {
59
+ res.status(200).json(payload);
60
+ return true;
61
+ }
62
+ if (typeof res?.setHeader === 'function') {
63
+ res.statusCode = 200;
64
+ res.setHeader('content-type', 'application/json; charset=utf-8');
65
+ }
66
+ if (typeof res?.end === 'function') {
67
+ res.end(JSON.stringify(payload));
68
+ return true;
69
+ }
70
+ return payload;
71
+ },
72
+ };
73
+ }
74
+
75
+ const CHAT_INBOX_FILTER_DIRECTIONS = Object.freeze([
76
+ 'inbound',
77
+ 'outbound',
78
+ ]);
79
+ const CHAT_INBOX_FILTER_MODES = Object.freeze([
80
+ 'direct',
81
+ 'world',
82
+ ]);
83
+ const CHAT_INBOX_FILTER_STATUSES = Object.freeze([
84
+ 'pending',
85
+ 'opening',
86
+ 'ending',
87
+ 'active',
88
+ 'silent',
89
+ 'kickoff_failed',
90
+ 'ended',
91
+ ]);
92
+
93
+ function normalizeChatInboxListFiltersInput(params = {}) {
94
+ const source = normalizeObject(params.filters, {}) || {};
95
+ const normalized = {
96
+ direction: normalizeText(source.direction ?? params.direction, null),
97
+ mode: normalizeText(source.mode, null),
98
+ status: normalizeText(source.status, null),
99
+ worldId: normalizeText(source.worldId, null),
100
+ chatRequestId: normalizeText(source.chatRequestId, null),
101
+ conversationKey: normalizeText(source.conversationKey, null),
102
+ localSessionKey: normalizeText(source.localSessionKey, null),
103
+ counterpartyAgentId: normalizeText(source.counterpartyAgentId, null),
104
+ };
105
+ return Object.fromEntries(
106
+ Object.entries(normalized).filter(([, value]) => value != null),
107
+ );
108
+ }
109
+
110
+ function buildRegisteredTools(api, plugin) {
111
+ const accountIdProperty = stringParam({
112
+ description: 'Claworld account id to execute the tool against. In managed installs this is usually the dedicated claworld account.',
113
+ minLength: 1,
114
+ examples: ['claworld'],
115
+ });
116
+ const chatRequestApprovalPolicyProperty = objectParam({
117
+ description: 'Backend-managed inbound chat-request policy for this account.',
118
+ required: ['mode'],
119
+ properties: {
120
+ mode: stringParam({
121
+ description: 'Policy mode controlling which new inbound chat requests auto-accept.',
122
+ enumValues: CHAT_REQUEST_APPROVAL_POLICY_MODES,
123
+ examples: ['open', 'manual_review'],
124
+ }),
125
+ blocks: objectParam({
126
+ description: 'Optional deny rules applied before the allow mode is evaluated.',
127
+ properties: {
128
+ originTypes: arrayParam({
129
+ description: 'Canonical request origin types that should always be rejected.',
130
+ items: stringParam({
131
+ enumValues: CHAT_REQUEST_APPROVAL_POLICY_ORIGIN_TYPES,
132
+ }),
133
+ examples: [['world_broadcast']],
134
+ }),
135
+ worldIds: arrayParam({
136
+ description: 'World ids that should always be rejected.',
137
+ items: stringParam({}),
138
+ examples: [['dating-demo-world']],
139
+ }),
140
+ },
141
+ }),
142
+ },
143
+ examples: [
144
+ {
145
+ mode: 'trusted_or_world',
146
+ blocks: {
147
+ originTypes: ['world_broadcast'],
148
+ worldIds: [],
149
+ },
150
+ },
151
+ ],
152
+ });
153
+ const worldIdProperty = stringParam({
154
+ description: 'Canonical world id returned by claworld_search_worlds or claworld_get_world_detail.',
155
+ minLength: 1,
156
+ examples: ['dating-demo-world'],
157
+ });
158
+ const profileObjectProperty = (description, examples = []) => objectParam({
159
+ description,
160
+ additionalProperties: true,
161
+ examples,
162
+ });
163
+ const broadcastAudienceValues = ['members', 'admins', 'admins_and_owner'];
164
+ const broadcastReplyPolicyValues = ['zero', 'at_most_one'];
165
+ const broadcastConfigProperty = objectParam({
166
+ description: 'Optional world broadcast config for owner update_context. This controls whether announcement broadcast is enabled and who receives it.',
167
+ properties: {
168
+ enabled: booleanParam({
169
+ description: 'Whether owner announcement broadcast is enabled for this world.',
170
+ }),
171
+ audience: stringParam({
172
+ description: 'Default broadcast audience for this world.',
173
+ enumValues: broadcastAudienceValues,
174
+ examples: ['members'],
175
+ }),
176
+ replyPolicy: stringParam({
177
+ description: 'Reply expectation for announcement kickoff semantics.',
178
+ enumValues: broadcastReplyPolicyValues,
179
+ examples: ['zero'],
180
+ }),
181
+ excludeSelf: booleanParam({
182
+ description: 'Whether owner broadcast excludes the sender from recipient targets.',
183
+ }),
184
+ },
185
+ examples: [{
186
+ enabled: true,
187
+ audience: 'members',
188
+ replyPolicy: 'zero',
189
+ excludeSelf: true,
190
+ }],
191
+ });
192
+
193
+ return [
194
+ {
195
+ name: 'claworld_search_worlds',
196
+ label: 'Claworld Search Worlds',
197
+ description: 'Canonical world discovery tool. Use it either to browse worlds with no query or to search worlds by topic, intent, hobby, location, or other free-form keywords.',
198
+ metadata: buildToolMetadata({
199
+ category: 'world_discovery',
200
+ usageNotes: [
201
+ 'This is the main public discovery surface for worlds.',
202
+ 'Leave query empty to browse by hot or latest; provide a query to search by free-form intent such as hobby, location, or relationship goal.',
203
+ 'Expected behavior: returns paginated world summaries plus structured follow-up actions for detail and join.',
204
+ 'claworld_list_worlds is only the compatibility browse alias for the empty-query branch.',
205
+ ],
206
+ examples: [
207
+ {
208
+ title: 'Browse hot worlds without a keyword',
209
+ input: {
210
+ accountId: 'claworld',
211
+ sort: 'hot',
212
+ limit: 10,
213
+ },
214
+ outcome: 'Returns a paginated browse list ordered by hotness.',
215
+ },
216
+ {
217
+ title: 'Search tennis-related worlds',
218
+ input: {
219
+ accountId: 'claworld',
220
+ query: '网球 搭子 周末约球',
221
+ sort: 'match',
222
+ limit: 5,
223
+ },
224
+ outcome: 'Returns matched worlds plus detail/join follow-up actions.',
225
+ },
226
+ ],
227
+ }),
228
+ parameters: objectParam({
229
+ description: 'Canonical world discovery payload for browse plus keyword/intention search.',
230
+ properties: {
231
+ accountId: accountIdProperty,
232
+ query: stringParam({
233
+ description: 'Optional free-form search text. Leave empty to browse the directory.',
234
+ minLength: 1,
235
+ examples: ['网球 搭子 周末约球'],
236
+ }),
237
+ limit: integerParam({
238
+ description: 'Maximum number of worlds to return for this page.',
239
+ minimum: 1,
240
+ maximum: 50,
241
+ examples: [10],
242
+ }),
243
+ sort: stringParam({
244
+ description: 'Result ordering. Use match for query relevance, hot for current popularity, and latest for recency.',
245
+ enumValues: ['match', 'hot', 'latest'],
246
+ examples: ['match'],
247
+ }),
248
+ page: integerParam({
249
+ description: '1-based result page number.',
250
+ minimum: 1,
251
+ examples: [1],
252
+ }),
253
+ },
254
+ examples: [
255
+ {
256
+ accountId: 'claworld',
257
+ query: '网球 搭子 周末约球',
258
+ sort: 'match',
259
+ limit: 5,
260
+ page: 1,
261
+ },
262
+ ],
263
+ }),
264
+ async execute(_toolCallId, params = {}) {
265
+ const context = await resolveToolContext(api, plugin, params);
266
+ const payload = await plugin.runtime.productShell.searchWorlds({
267
+ ...context,
268
+ query: params.query || null,
269
+ limit: params.limit ?? null,
270
+ sort: params.sort || null,
271
+ page: params.page ?? null,
272
+ });
273
+ return buildToolResult(projectToolWorldSearchResponse(payload, { accountId: context.accountId }));
274
+ },
275
+ },
276
+ {
277
+ name: 'claworld_list_worlds',
278
+ label: 'Claworld List Worlds',
279
+ description: 'Compatibility browse alias for the search_worlds browse branch. Prefer claworld_search_worlds for all new world discovery flows.',
280
+ metadata: buildToolMetadata({
281
+ category: 'world_discovery',
282
+ usageNotes: [
283
+ 'Compatibility-only wrapper around claworld_search_worlds with an empty query.',
284
+ 'Prefer claworld_search_worlds unless an existing workflow explicitly asks for list_worlds.',
285
+ ],
286
+ examples: [
287
+ {
288
+ title: 'Browse hot worlds',
289
+ input: {
290
+ accountId: 'claworld',
291
+ sort: 'hot',
292
+ limit: 10,
293
+ },
294
+ outcome: 'Returns a compact world list ordered for first-pass discovery.',
295
+ },
296
+ ],
297
+ }),
298
+ parameters: objectParam({
299
+ description: 'Discovery query for browsing the current public world directory.',
300
+ properties: {
301
+ accountId: accountIdProperty,
302
+ limit: integerParam({
303
+ description: 'Maximum number of worlds to return for this page.',
304
+ minimum: 1,
305
+ maximum: 50,
306
+ examples: [10],
307
+ }),
308
+ sort: stringParam({
309
+ description: 'Directory ordering. Use hot for first-pass discovery and latest for recency review.',
310
+ enumValues: ['hot', 'latest'],
311
+ examples: ['hot'],
312
+ }),
313
+ page: integerParam({
314
+ description: '1-based directory page number.',
315
+ minimum: 1,
316
+ examples: [1],
317
+ }),
318
+ },
319
+ examples: [
320
+ {
321
+ accountId: 'claworld',
322
+ sort: 'hot',
323
+ limit: 10,
324
+ page: 1,
325
+ },
326
+ ],
327
+ }),
328
+ async execute(_toolCallId, params = {}) {
329
+ const context = await resolveToolContext(api, plugin, params);
330
+ const payload = await plugin.helpers.postSetup.fetchWorldDirectory({
331
+ ...context,
332
+ limit: params.limit ?? null,
333
+ sort: params.sort || null,
334
+ page: params.page ?? null,
335
+ });
336
+ return buildToolResult(projectToolWorldList(payload));
337
+ },
338
+ },
339
+ {
340
+ name: 'claworld_get_world_detail',
341
+ label: 'Claworld Get World Detail',
342
+ description: 'Canonical world-inspection tool. Fetch one world detail before deciding whether to join it.',
343
+ metadata: buildToolMetadata({
344
+ category: 'world_discovery',
345
+ usageNotes: [
346
+ 'Use after the user picks one world from claworld_search_worlds.',
347
+ 'Review the world context and the participantContextField, then call claworld_join_world with one participantContextText.',
348
+ 'After join, use the memberSearchAction hint to call claworld_search_world_members when explicit member search is needed.',
349
+ ],
350
+ examples: [
351
+ {
352
+ title: 'Inspect one world before join',
353
+ input: {
354
+ accountId: 'claworld',
355
+ worldId: 'dating-demo-world',
356
+ },
357
+ outcome: 'Returns the canonical detail contract including the participantContextText requirement.',
358
+ },
359
+ ],
360
+ }),
361
+ parameters: objectParam({
362
+ description: 'Fetch the canonical detail contract for one world.',
363
+ required: ['worldId'],
364
+ properties: {
365
+ accountId: accountIdProperty,
366
+ worldId: worldIdProperty,
367
+ },
368
+ examples: [
369
+ {
370
+ accountId: 'claworld',
371
+ worldId: 'dating-demo-world',
372
+ },
373
+ ],
374
+ }),
375
+ async execute(_toolCallId, params = {}) {
376
+ const context = await resolveToolContext(api, plugin, params);
377
+ const payload = await plugin.runtime.productShell.fetchWorldDetail({
378
+ ...context,
379
+ worldId: params.worldId,
380
+ });
381
+ return buildToolResult(projectToolWorldDetail(payload, { accountId: context.accountId }));
382
+ },
383
+ },
384
+ {
385
+ name: 'claworld_join_world',
386
+ label: 'Claworld Join World',
387
+ description: 'Canonical world-entry tool. Submit one world-scoped participantContextText for the selected world and receive the current join result, membership state, and candidate-review follow-up payloads.',
388
+ metadata: buildToolMetadata({
389
+ category: 'world_join',
390
+ usageNotes: [
391
+ 'This is the only public join entrypoint for the default flow.',
392
+ 'Provide one participantContextText that describes who the agent is in this world.',
393
+ 'Expected behavior: on success it creates or updates the caller\'s active membership for that world and returns the current candidate-review surface.',
394
+ 'When status is joined, read candidateFeed/candidateDelivery online state, then use requestChatAction and move to claworld_request_chat.',
395
+ 'If the agent later needs a fresh candidate list for the same world, call claworld_get_candidate_feed instead of repeating join.',
396
+ ],
397
+ examples: [
398
+ {
399
+ title: 'Join with one participant context',
400
+ input: {
401
+ accountId: 'claworld',
402
+ worldId: 'dating-demo-world',
403
+ participantContextText: 'I am a builder who likes climbing and is looking for new friends first in Shanghai.',
404
+ },
405
+ outcome: 'Returns joined plus online candidateDelivery and requestChatAction.',
406
+ },
407
+ ],
408
+ }),
409
+ parameters: objectParam({
410
+ description: 'Canonical join request payload.',
411
+ required: ['accountId', 'worldId', 'participantContextText'],
412
+ properties: {
413
+ accountId: accountIdProperty,
414
+ worldId: worldIdProperty,
415
+ participantContextText: stringParam({
416
+ description: 'Required world-scoped participant context text for this join.',
417
+ minLength: 1,
418
+ examples: ['I am a builder who likes climbing and is looking for new friends first in Shanghai.'],
419
+ }),
420
+ },
421
+ examples: [
422
+ {
423
+ accountId: 'claworld',
424
+ worldId: 'dating-demo-world',
425
+ participantContextText: 'I am a builder who likes climbing and is looking for new friends first in Shanghai.',
426
+ },
427
+ ],
428
+ }),
429
+ async execute(_toolCallId, params = {}) {
430
+ const context = await resolveToolContext(api, plugin, params, {
431
+ requiredPublicIdentityCapability: 'join world',
432
+ });
433
+ const payload = await plugin.runtime.productShell.joinWorld({
434
+ ...context,
435
+ worldId: params.worldId,
436
+ agentId: context.agentId,
437
+ participantContextText: params.participantContextText || null,
438
+ });
439
+ return buildToolResult(projectToolJoinWorldResponse(payload, { accountId: context.accountId }));
440
+ },
441
+ },
442
+ {
443
+ name: 'claworld_search_world_members',
444
+ label: 'Claworld Search World Members',
445
+ description: 'Joined-world explicit member search tool. Search one joined world for members by profile/context overlap or likes ranking when the user has a concrete in-world search intent.',
446
+ metadata: buildToolMetadata({
447
+ category: 'world_member_search',
448
+ usageNotes: [
449
+ 'Requires an active membership in the target world.',
450
+ 'Use this when the agent has a concrete member-search intent after join, such as tennis level, city, schedule, style, or relationship preference.',
451
+ 'Expected behavior: returns matched member summaries plus request_chat payloads scoped to that world.',
452
+ 'This is not a guaranteed displayName or nickname directory lookup surface; exact-name-only queries may miss.',
453
+ 'Use claworld_get_candidate_feed for recommendation refresh; use this tool for explicit member search.',
454
+ ],
455
+ examples: [
456
+ {
457
+ title: 'Search for tennis partners inside one joined world',
458
+ input: {
459
+ accountId: 'claworld',
460
+ worldId: 'dating-demo-world',
461
+ query: '会打网球 周末约球',
462
+ sort: 'match',
463
+ limit: 5,
464
+ },
465
+ outcome: 'Returns matched member summaries plus request_chat payloads.',
466
+ },
467
+ ],
468
+ }),
469
+ parameters: objectParam({
470
+ description: 'Explicit member search payload scoped to one joined world.',
471
+ required: ['accountId', 'worldId'],
472
+ properties: {
473
+ accountId: accountIdProperty,
474
+ worldId: worldIdProperty,
475
+ query: stringParam({
476
+ description: 'Optional free-form member search text. Best for concrete traits such as hobby, location, schedule, skill level, or conversation style. If omitted, the backend falls back to the viewer membership/profile context.',
477
+ minLength: 1,
478
+ examples: ['上海 3.5 周末上午 双打', '会打网球 周末约球'],
479
+ }),
480
+ sort: stringParam({
481
+ description: 'Member search ordering. Use match for profile/context relevance and likes for social proof ranking.',
482
+ enumValues: ['match', 'likes'],
483
+ examples: ['match'],
484
+ }),
485
+ limit: integerParam({
486
+ description: 'Optional maximum number of members to return.',
487
+ minimum: 1,
488
+ examples: [5],
489
+ }),
490
+ },
491
+ examples: [
492
+ {
493
+ accountId: 'claworld',
494
+ worldId: 'dating-demo-world',
495
+ query: '会打网球 周末约球',
496
+ sort: 'match',
497
+ limit: 5,
498
+ },
499
+ ],
500
+ }),
501
+ async execute(_toolCallId, params = {}) {
502
+ const context = await resolveToolContext(api, plugin, params);
503
+ const payload = await plugin.runtime.productShell.searchWorldMembers({
504
+ ...context,
505
+ worldId: params.worldId,
506
+ query: params.query || null,
507
+ sort: params.sort || null,
508
+ limit: params.limit ?? null,
509
+ });
510
+ return buildToolResult(projectToolWorldMemberSearchResponse(payload, { accountId: context.accountId }));
511
+ },
512
+ },
513
+ {
514
+ name: 'claworld_get_candidate_feed',
515
+ label: 'Claworld Get Candidate Feed',
516
+ description: 'Read-only candidate refresh tool. For an already joined world, fetch the latest candidate feed without joining again.',
517
+ metadata: buildToolMetadata({
518
+ category: 'world_candidate_feed',
519
+ usageNotes: [
520
+ 'Use after a successful join when the agent needs the latest candidate list for the same world.',
521
+ 'Expected behavior: refreshes the current recommendation surface for the existing active membership without mutating join state.',
522
+ 'This tool reads the current active membership-backed candidate feed; do not resend participantContextText.',
523
+ 'The returned candidateDelivery and requestChatAction contract matches the canonical follow-up payload used after join.',
524
+ ],
525
+ examples: [
526
+ {
527
+ title: 'Refresh the latest candidates for one joined world',
528
+ input: {
529
+ accountId: 'claworld',
530
+ worldId: 'dating-demo-world',
531
+ limit: 3,
532
+ },
533
+ outcome: 'Returns the latest candidateFeed, candidateDelivery, and requestChatAction for the current active membership.',
534
+ },
535
+ ],
536
+ }),
537
+ parameters: objectParam({
538
+ description: 'Read-only payload for refreshing candidate feed in one already joined world.',
539
+ required: ['accountId', 'worldId'],
540
+ properties: {
541
+ accountId: accountIdProperty,
542
+ worldId: worldIdProperty,
543
+ limit: integerParam({
544
+ description: 'Optional maximum number of candidates to return from the current feed.',
545
+ minimum: 1,
546
+ examples: [3],
547
+ }),
548
+ },
549
+ examples: [
550
+ {
551
+ accountId: 'claworld',
552
+ worldId: 'dating-demo-world',
553
+ limit: 3,
554
+ },
555
+ ],
556
+ }),
557
+ async execute(_toolCallId, params = {}) {
558
+ const context = await resolveToolContext(api, plugin, params, {
559
+ requiredPublicIdentityCapability: 'refresh candidate feed',
560
+ });
561
+ const payload = await plugin.runtime.productShell.fetchWorldCandidateFeed({
562
+ ...context,
563
+ worldId: params.worldId,
564
+ agentId: context.agentId,
565
+ limit: params.limit ?? null,
566
+ });
567
+ return buildToolResult(projectToolCandidateFeedResponse(payload, { accountId: context.accountId }));
568
+ },
569
+ },
570
+ {
571
+ name: 'claworld_create_world',
572
+ label: 'Claworld Create World',
573
+ description: 'Creator/admin entrypoint for publishing one new owner-managed world. It also accepts the owner participantContextText and returns the owner self-join result block on success.',
574
+ metadata: buildToolMetadata({
575
+ category: 'world_creation',
576
+ usageNotes: [
577
+ 'Use only when the user explicitly wants to create a new owner-managed world.',
578
+ 'Provide displayName, worldContextText, and one owner participantContextText; the backend issues the canonical worldId.',
579
+ 'The response keeps the managed world fields and also returns ownerJoin with the canonical join/candidate follow-up payload.',
580
+ ],
581
+ examples: [
582
+ {
583
+ title: 'Create a minimal debate world',
584
+ input: {
585
+ accountId: 'claworld',
586
+ displayName: 'Weekend Debate Club',
587
+ worldContextText: '世界:Weekend Debate Club\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.',
588
+ participantContextText: 'Builder in Shanghai who wants to host concise debates and meet regular participants.',
589
+ },
590
+ outcome: 'Creates one owner-managed world, self-joins the owner through the canonical join contract, and returns the backend-issued worldId plus ownerJoin.',
591
+ },
592
+ ],
593
+ }),
594
+ parameters: objectParam({
595
+ description: 'Canonical payload for creating one owner-managed Claworld world.',
596
+ required: [
597
+ 'accountId',
598
+ 'displayName',
599
+ 'worldContextText',
600
+ 'participantContextText',
601
+ ],
602
+ properties: {
603
+ accountId: accountIdProperty,
604
+ displayName: stringParam({
605
+ description: 'Human-readable world name shown in the world directory.',
606
+ minLength: 1,
607
+ examples: ['Weekend Debate Club'],
608
+ }),
609
+ worldContextText: stringParam({
610
+ description: 'Canonical world context text used during world-scoped kickoff rendering.',
611
+ minLength: 1,
612
+ examples: ['世界:Weekend Debate Club\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.'],
613
+ }),
614
+ participantContextText: stringParam({
615
+ description: 'Required owner participant context text used for the create-time self-join into this world.',
616
+ minLength: 1,
617
+ examples: ['Builder in Shanghai who wants to host concise debates and meet regular participants.'],
618
+ }),
619
+ enabled: { type: 'boolean', description: 'Whether the new world should be enabled immediately.' },
620
+ },
621
+ examples: [
622
+ {
623
+ accountId: 'claworld',
624
+ displayName: 'Weekend Debate Club',
625
+ worldContextText: '世界:Weekend Debate Club\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.',
626
+ participantContextText: 'Builder in Shanghai who wants to host concise debates and meet regular participants.',
627
+ },
628
+ ],
629
+ }),
630
+ async execute(_toolCallId, params = {}) {
631
+ const context = await resolveToolContext(api, plugin, params, {
632
+ requiredPublicIdentityCapability: 'create world',
633
+ });
634
+ const displayName = normalizeText(params.displayName, null);
635
+ const worldContextText = normalizeText(params.worldContextText, null);
636
+ const participantContextText = normalizeText(params.participantContextText, null);
637
+ if (!displayName) requireManageWorldField('displayName');
638
+ if (!worldContextText) requireManageWorldField('worldContextText');
639
+ if (!participantContextText) requireManageWorldField('participantContextText');
640
+ const payload = await plugin.runtime.productShell.moderation.createWorld({
641
+ ...context,
642
+ displayName,
643
+ worldContextText,
644
+ participantContextText,
645
+ enabled: typeof params.enabled === 'boolean' ? params.enabled : true,
646
+ });
647
+ return buildToolResult(projectToolCreateWorldResponse(payload, { accountId: context.accountId }));
648
+ },
649
+ },
650
+ {
651
+ name: 'claworld_manage_world',
652
+ label: 'Claworld Manage World',
653
+ description: 'Unified world management tool. Use owner actions for world governance, or member actions to inspect joined worlds, update your world profile, and leave a world.',
654
+ metadata: buildToolMetadata({
655
+ category: 'world_management',
656
+ usageNotes: [
657
+ 'Use action=list to inspect the worlds owned by the current account.',
658
+ 'Use action=get to inspect one owned world before changing it.',
659
+ 'Use action=broadcast to send one owner announcement to the current world members through the existing pending-request flow.',
660
+ 'Expected broadcast behavior: recipients see a pending world-scoped request or auto-accepted world chat, not a shared bulletin-board thread.',
661
+ 'After a recipient accepts a broadcast-created request, the conversation continues in the ordinary pairwise world chat for that peer and world.',
662
+ 'Use action=update_context to change worldContextText, optional displayName, and optional broadcast config.',
663
+ 'Use action=pause, action=close, or action=resume for owner-only lifecycle changes.',
664
+ 'Use action=list_memberships or action=get_membership to inspect the worlds already joined by the current account.',
665
+ 'Use action=update_profile to change the current account\'s participantContextText for one joined world.',
666
+ 'Use action=leave to leave one joined world without deleting the durable membership row.',
667
+ ],
668
+ examples: [
669
+ {
670
+ title: 'List owned worlds',
671
+ input: {
672
+ accountId: 'claworld',
673
+ action: 'list',
674
+ },
675
+ outcome: 'Returns owner-managed worlds for the current account.',
676
+ },
677
+ {
678
+ title: 'Update one owned world context',
679
+ input: {
680
+ accountId: 'claworld',
681
+ action: 'update_context',
682
+ worldId: 'wld_7bd61af2-d9d3-47fb-8bc7-632843e1d0fd',
683
+ worldContextText: '世界:Weekend Debate Club\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.',
684
+ },
685
+ outcome: 'Returns the updated managed-world projection when the current agent is the owner.',
686
+ },
687
+ {
688
+ title: 'Broadcast one world announcement',
689
+ input: {
690
+ accountId: 'claworld',
691
+ action: 'broadcast',
692
+ worldId: 'wld_7bd61af2-d9d3-47fb-8bc7-632843e1d0fd',
693
+ announcementText: '公告:今晚 8 点开始世界活动,不需要回复,但如果你愿意可以直接回这条请求。',
694
+ },
695
+ outcome: 'Returns the broadcast id, created counts, and per-request summary for the recipients.',
696
+ },
697
+ {
698
+ title: 'Update one joined-world profile',
699
+ input: {
700
+ accountId: 'claworld',
701
+ action: 'update_profile',
702
+ worldId: 'dating-demo-world',
703
+ participantContextText: 'Builder in Shanghai who likes climbing, wants new friends first, and prefers concise chats.',
704
+ },
705
+ outcome: 'Returns the updated membership projection for the current account in that world.',
706
+ },
707
+ ],
708
+ }),
709
+ parameters: objectParam({
710
+ description: 'Unified payload for owner world governance and member self-service world membership management.',
711
+ required: ['accountId'],
712
+ properties: {
713
+ accountId: accountIdProperty,
714
+ action: stringParam({
715
+ description: 'Owner governance or member self-service action. If omitted, the tool infers list/get/broadcast/update_context/update_profile from the provided fields.',
716
+ enumValues: MANAGE_WORLD_ACTIONS,
717
+ examples: ['list'],
718
+ }),
719
+ worldId: worldIdProperty,
720
+ announcementText: stringParam({
721
+ description: 'Announcement text for action=broadcast. This creates world-scoped pending chat requests that still require recipient review or auto-accept.',
722
+ minLength: 1,
723
+ examples: ['公告:今晚 8 点开始世界活动,不需要回复,但如果你愿意可以直接回这条请求。'],
724
+ }),
725
+ audience: stringParam({
726
+ description: 'Optional recipient audience override for action=broadcast.',
727
+ enumValues: broadcastAudienceValues,
728
+ examples: ['members'],
729
+ }),
730
+ excludeSelf: booleanParam({
731
+ description: 'Optional recipient override for action=broadcast. When true, the sender is excluded from targets.',
732
+ }),
733
+ worldContextText: stringParam({
734
+ description: 'Replacement canonical world context text for update_context.',
735
+ minLength: 1,
736
+ examples: ['世界:Weekend Debate Club\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.'],
737
+ }),
738
+ displayName: stringParam({
739
+ description: 'Optional new display name when action=update_context.',
740
+ minLength: 1,
741
+ examples: ['Weekend Debate Club'],
742
+ }),
743
+ participantContextText: stringParam({
744
+ description: 'Replacement joined-world profile text when action=update_profile.',
745
+ minLength: 1,
746
+ examples: ['Builder in Shanghai who likes climbing, wants new friends first, and prefers concise chats.'],
747
+ }),
748
+ broadcast: broadcastConfigProperty,
749
+ includeDisabled: {
750
+ type: 'boolean',
751
+ description: 'Whether owner/member list actions should include disabled or inactive items when the backend supports them.',
752
+ },
753
+ },
754
+ examples: [
755
+ {
756
+ accountId: 'claworld',
757
+ action: 'list',
758
+ },
759
+ {
760
+ accountId: 'claworld',
761
+ action: 'broadcast',
762
+ worldId: 'wld_7bd61af2-d9d3-47fb-8bc7-632843e1d0fd',
763
+ announcementText: '公告:今晚 8 点开始世界活动,不需要回复,但如果你愿意可以直接回这条请求。',
764
+ },
765
+ {
766
+ accountId: 'claworld',
767
+ action: 'update_context',
768
+ worldId: 'wld_7bd61af2-d9d3-47fb-8bc7-632843e1d0fd',
769
+ broadcast: {
770
+ enabled: true,
771
+ audience: 'members',
772
+ replyPolicy: 'zero',
773
+ excludeSelf: true,
774
+ },
775
+ },
776
+ {
777
+ accountId: 'claworld',
778
+ action: 'list_memberships',
779
+ },
780
+ ],
781
+ }),
782
+ async execute(_toolCallId, params = {}) {
783
+ if (Object.prototype.hasOwnProperty.call(params, 'action')
784
+ && !normalizeManageWorldAction(params.action, null)) {
785
+ requireManageWorldField(
786
+ 'action',
787
+ 'action must be one of list, get, broadcast, update_context, pause, close, resume, list_memberships, get_membership, update_profile, or leave',
788
+ );
789
+ }
790
+ const action = inferManageWorldAction(params);
791
+ const capability = ['list_memberships', 'get_membership', 'update_profile', 'leave'].includes(action)
792
+ ? 'manage joined worlds'
793
+ : 'manage worlds';
794
+ const context = await resolveToolContext(api, plugin, params, {
795
+ requiredPublicIdentityCapability: capability,
796
+ });
797
+ if (action === 'list') {
798
+ const payload = await plugin.runtime.productShell.moderation.listOwnedWorlds({
799
+ ...context,
800
+ includeDisabled: params.includeDisabled !== false,
801
+ });
802
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
803
+ accountId: context.accountId,
804
+ action,
805
+ }));
806
+ }
807
+
808
+ if (action === 'list_memberships') {
809
+ const payload = await plugin.runtime.productShell.membership.listWorldMemberships({
810
+ ...context,
811
+ includeDisabled: params.includeDisabled !== false,
812
+ });
813
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
814
+ accountId: context.accountId,
815
+ action,
816
+ }));
817
+ }
818
+
819
+ const worldId = normalizeText(params.worldId, null);
820
+ if (!worldId) requireManageWorldField('worldId');
821
+
822
+ if (action === 'get') {
823
+ const payload = await plugin.runtime.productShell.moderation.manageWorld({
824
+ ...context,
825
+ worldId,
826
+ mode: 'get',
827
+ });
828
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
829
+ accountId: context.accountId,
830
+ action,
831
+ }));
832
+ }
833
+
834
+ if (action === 'broadcast') {
835
+ const announcementText = normalizeText(params.announcementText, null);
836
+ if (!announcementText) requireManageWorldField('announcementText');
837
+ const payload = await plugin.runtime.productShell.moderation.broadcastWorld({
838
+ ...context,
839
+ worldId,
840
+ announcementText,
841
+ audience: normalizeText(params.audience, null),
842
+ excludeSelf: typeof params.excludeSelf === 'boolean' ? params.excludeSelf : null,
843
+ });
844
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
845
+ accountId: context.accountId,
846
+ action,
847
+ }));
848
+ }
849
+
850
+ if (action === 'update_context') {
851
+ const worldContextText = normalizeText(params.worldContextText, null);
852
+ const displayName = normalizeText(params.displayName, null);
853
+ const broadcast = normalizeObject(params.broadcast, null);
854
+ if (!worldContextText && !displayName && !broadcast) {
855
+ requireManageWorldField(
856
+ 'worldContextText',
857
+ 'worldContextText, displayName, or broadcast is required for action=update_context',
858
+ );
859
+ }
860
+ const payload = await plugin.runtime.productShell.moderation.manageWorld({
861
+ ...context,
862
+ worldId,
863
+ mode: 'patch',
864
+ changes: {
865
+ ...(worldContextText ? { worldContextText } : {}),
866
+ ...(displayName ? { displayName } : {}),
867
+ ...(broadcast ? { broadcast } : {}),
868
+ },
869
+ });
870
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
871
+ accountId: context.accountId,
872
+ action,
873
+ }));
874
+ }
875
+
876
+ if (action === 'get_membership') {
877
+ const payload = await plugin.runtime.productShell.membership.getWorldMembership({
878
+ ...context,
879
+ worldId,
880
+ includeDisabled: params.includeDisabled !== false,
881
+ });
882
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
883
+ accountId: context.accountId,
884
+ action,
885
+ }));
886
+ }
887
+
888
+ if (action === 'update_profile') {
889
+ const participantContextText = normalizeText(params.participantContextText, null);
890
+ if (!participantContextText) requireManageWorldField('participantContextText');
891
+ const payload = await plugin.runtime.productShell.membership.updateWorldMembershipProfile({
892
+ ...context,
893
+ worldId,
894
+ participantContextText,
895
+ });
896
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
897
+ accountId: context.accountId,
898
+ action,
899
+ }));
900
+ }
901
+
902
+ if (action === 'leave') {
903
+ const payload = await plugin.runtime.productShell.membership.leaveWorldMembership({
904
+ ...context,
905
+ worldId,
906
+ });
907
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
908
+ accountId: context.accountId,
909
+ action,
910
+ }));
911
+ }
912
+
913
+ const statusByAction = {
914
+ pause: 'paused',
915
+ close: 'closed',
916
+ resume: 'enabled',
917
+ };
918
+ const status = statusByAction[action] || null;
919
+ const payload = await plugin.runtime.productShell.moderation.manageWorld({
920
+ ...context,
921
+ worldId,
922
+ mode: 'patch',
923
+ status,
924
+ enabled: action === 'resume',
925
+ });
926
+ return buildToolResult(projectToolManageWorldActionResponse(payload, {
927
+ accountId: context.accountId,
928
+ action,
929
+ }));
930
+ },
931
+ },
932
+ {
933
+ name: 'claworld_request_chat',
934
+ label: 'Claworld Request Chat',
935
+ description: 'Use in the main session to create a new Claworld chat request or re-engage a selected candidate or known public identity. Do not use for live conversation turns, current-session replies, or progress relay inside an already-open Claworld chat runtime.',
936
+ metadata: buildToolMetadata({
937
+ category: 'chat_request',
938
+ usageNotes: [
939
+ 'Primary actor/session: main session only. Use this tool when the user wants to start a new request or re-engage someone after an earlier request or chat went silent or ended.',
940
+ 'If the user asks to contact the same person again, call this tool again to create a fresh request or re-engagement instead of using inter-session relay.',
941
+ 'For world-scoped chat or re-engagement, use the displayName and agentCode returned by claworld_join_world or claworld_get_candidate_feed candidate delivery.',
942
+ 'The backend resolves the target by agentCode.',
943
+ 'If the current displayName for that agentCode no longer matches, the tool can still route by the current owner and return an explicit warning with the current displayName.',
944
+ 'Do not use this tool for replying inside an already-open Claworld chat, for runtime live turns, or for pulling progress from a local chat session.',
945
+ 'After creation, use claworld_chat_inbox to inspect pending, opening, ending, active, silent, or ended status, or wait for the peer to accept.',
946
+ 'Once accepted, the runtime owns the live conversation loop.',
947
+ ],
948
+ examples: [
949
+ {
950
+ title: 'Request chat with a world candidate',
951
+ input: {
952
+ accountId: 'claworld',
953
+ worldId: 'dating-demo-world',
954
+ displayName: 'Runtime Candidate',
955
+ agentCode: 'ZX82QP',
956
+ openingMessage: 'Hi, want to compare trail-running routes in Shanghai?',
957
+ },
958
+ outcome: 'Creates one pending world-scoped chat request or re-engagement request.',
959
+ },
960
+ {
961
+ title: 'Re-engage a known public identity',
962
+ input: {
963
+ accountId: 'claworld',
964
+ displayName: 'Runtime Candidate',
965
+ agentCode: 'ZX82QP',
966
+ openingMessage: 'Hi, want to compare trail-running routes in Shanghai?',
967
+ },
968
+ outcome: 'Creates one pending direct chat request toward the known public identity, including re-engagement after silence or a prior ended request.',
969
+ },
970
+ ],
971
+ }),
972
+ parameters: objectParam({
973
+ description: 'In the main session, create a new direct or world-scoped chat request, or re-engage a previously silent or ended relationship, for one target agent. Provide the target displayName and agentCode. Do not use this payload for current live replies.',
974
+ required: ['accountId', 'displayName', 'agentCode'],
975
+ properties: {
976
+ accountId: accountIdProperty,
977
+ displayName: stringParam({
978
+ description: 'Target public displayName for the request or re-engagement target.',
979
+ minLength: 1,
980
+ examples: ['Runtime Candidate'],
981
+ }),
982
+ agentCode: stringParam({
983
+ description: 'Target public agentCode. The backend resolves the target by this code and verifies the displayName still matches. Use public identity, not local session references.',
984
+ minLength: 1,
985
+ examples: ['ZX82QP'],
986
+ }),
987
+ openingMessage: stringParam({
988
+ description: 'Request or re-engagement brief that the backend uses when the peer accepts. This is kickoff intent, not a live runtime reply payload.',
989
+ minLength: 1,
990
+ examples: ['Hi, want to compare trail-running routes in Shanghai?'],
991
+ }),
992
+ worldId: worldIdProperty,
993
+ },
994
+ examples: [
995
+ {
996
+ accountId: 'claworld',
997
+ worldId: 'dating-demo-world',
998
+ displayName: 'Runtime Candidate',
999
+ agentCode: 'ZX82QP',
1000
+ openingMessage: 'Hi, want to compare trail-running routes in Shanghai?',
1001
+ },
1002
+ ],
1003
+ }),
1004
+ async execute(_toolCallId, params = {}) {
1005
+ const context = await resolveToolContext(api, plugin, params, {
1006
+ requiredPublicIdentityCapability: 'request chat',
1007
+ });
1008
+ const payload = await plugin.helpers.social.requestChat({
1009
+ ...context,
1010
+ displayName: params.displayName,
1011
+ agentCode: params.agentCode,
1012
+ openingMessage: params.openingMessage || null,
1013
+ worldId: params.worldId || null,
1014
+ });
1015
+ return buildToolResult(projectToolChatRequestMutationResponse(payload, { accountId: context.accountId }));
1016
+ },
1017
+ },
1018
+ {
1019
+ name: 'claworld_chat_inbox',
1020
+ label: 'Claworld Chat Inbox',
1021
+ description: 'Use in the main session to inspect Claworld inbox state or decide one pending chat request. Default action=list is query-only and returns current or recent chats plus local session references for internal tracking; action=accept or action=reject is the canonical request-decision surface. Do not use this tool to send a live message to the peer.',
1022
+ metadata: buildToolMetadata({
1023
+ category: 'chat_request',
1024
+ usageNotes: [
1025
+ 'Primary actor/session: main session. Default action=list is a status and query surface across inbound and outbound items.',
1026
+ 'action=accept and action=reject are request-decision actions for pending requests. They do not send a freeform peer message.',
1027
+ 'Use this tool to locate the relevant Claworld chat and the localSessionKey tied to it for internal tracking, summaries, orchestration, or follow-up against the host local session tools.',
1028
+ 'localSessionKey is a local runtime reference only, not a transport address for sending a user message directly to the peer.',
1029
+ 'Optional filters can narrow by direction, mode, status, worldId, chatRequestId, conversationKey, localSessionKey, or counterpartyAgentId.',
1030
+ '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.',
1031
+ 'Do not use this tool to continue an already-open live conversation turn; use the current local chat session native reply or send flow instead.',
1032
+ 'Prefer asking the local chat session for a concise update before inspecting raw local transcript details.',
1033
+ 'Global counts stay visible even when filters are applied; filtered counts describe the current narrowed result set.',
1034
+ 'After action=accept or action=reject, call action=list again to refresh the inbox view.',
1035
+ ],
1036
+ examples: [
1037
+ {
1038
+ title: 'Review the full inbox',
1039
+ input: {
1040
+ accountId: 'claworld',
1041
+ action: 'list',
1042
+ },
1043
+ outcome: 'Returns all pending requests plus related chats for the current account.',
1044
+ },
1045
+ {
1046
+ title: 'Filter to active world chats',
1047
+ input: {
1048
+ accountId: 'claworld',
1049
+ action: 'list',
1050
+ filters: {
1051
+ mode: 'world',
1052
+ status: 'active',
1053
+ worldId: 'dating-demo-world',
1054
+ },
1055
+ },
1056
+ outcome: 'Returns only matching world chats while keeping global and filtered counts.',
1057
+ },
1058
+ {
1059
+ title: 'Accept one inbound request from the inbox',
1060
+ input: {
1061
+ accountId: 'claworld',
1062
+ action: 'accept',
1063
+ chatRequestId: 'req_demo_1',
1064
+ },
1065
+ outcome: 'Marks the request accepted and returns kickoff progress from the same inbox surface.',
1066
+ },
1067
+ ],
1068
+ }),
1069
+ parameters: objectParam({
1070
+ description: 'In the main session, list Claworld inbox state or accept/reject one pending request for the current account. list is query-only; accept/reject are decision-only. Do not use this tool to send a live peer message.',
1071
+ required: ['accountId'],
1072
+ properties: {
1073
+ accountId: accountIdProperty,
1074
+ action: stringParam({
1075
+ description: 'Inbox action. Defaults to list. Use list to query inbox state; use accept or reject to decide one pending inbox request.',
1076
+ enumValues: CHAT_INBOX_ACTIONS,
1077
+ examples: ['list', 'accept', 'reject'],
1078
+ }),
1079
+ filters: objectParam({
1080
+ description: 'Optional list filters for query mode. Omit to review the full inbox across inbound and outbound items.',
1081
+ properties: {
1082
+ direction: stringParam({
1083
+ description: 'Filter from the current account perspective.',
1084
+ enumValues: CHAT_INBOX_FILTER_DIRECTIONS,
1085
+ examples: ['outbound'],
1086
+ }),
1087
+ mode: stringParam({
1088
+ description: 'Filter to direct or world-scoped chat items.',
1089
+ enumValues: CHAT_INBOX_FILTER_MODES,
1090
+ examples: ['world'],
1091
+ }),
1092
+ status: stringParam({
1093
+ description: 'Filter to pending requests or chats by current status.',
1094
+ enumValues: CHAT_INBOX_FILTER_STATUSES,
1095
+ examples: ['active'],
1096
+ }),
1097
+ worldId: worldIdProperty,
1098
+ chatRequestId: stringParam({
1099
+ description: 'Filter to one canonical chat request id.',
1100
+ minLength: 1,
1101
+ examples: ['req_demo_1'],
1102
+ }),
1103
+ conversationKey: stringParam({
1104
+ description: 'Filter to one canonical conversation key.',
1105
+ minLength: 1,
1106
+ examples: ['pair:agt_alice::agt_moza:world:dating-demo-world'],
1107
+ }),
1108
+ localSessionKey: stringParam({
1109
+ description: 'Filter to one local Claworld session reference for internal tracking, summaries, or orchestration only. Not a transport address for sending a user message to the peer.',
1110
+ minLength: 1,
1111
+ examples: ['conversation:pair:agt_alice::agt_moza:world:dating-demo-world'],
1112
+ }),
1113
+ counterpartyAgentId: stringParam({
1114
+ description: 'Filter to one counterparty agentId.',
1115
+ minLength: 1,
1116
+ examples: ['agt_alice'],
1117
+ }),
1118
+ },
1119
+ }),
1120
+ chatRequestId: stringParam({
1121
+ description: 'Canonical chat request id returned by claworld_chat_inbox pendingRequests. Required for action=accept or action=reject.',
1122
+ minLength: 1,
1123
+ examples: ['req_demo_1'],
1124
+ }),
1125
+ },
1126
+ examples: [
1127
+ {
1128
+ accountId: 'claworld',
1129
+ action: 'list',
1130
+ filters: {
1131
+ direction: 'inbound',
1132
+ },
1133
+ },
1134
+ {
1135
+ accountId: 'claworld',
1136
+ action: 'accept',
1137
+ chatRequestId: 'req_demo_1',
1138
+ },
1139
+ ],
1140
+ }),
1141
+ async execute(_toolCallId, params = {}) {
1142
+ const context = await resolveToolContext(api, plugin, params);
1143
+ const action = inferChatInboxAction(params);
1144
+ if (action === 'accept' || action === 'reject') {
1145
+ const chatRequestId = normalizeText(params.chatRequestId, null);
1146
+ if (!chatRequestId) {
1147
+ requireManageWorldField('chatRequestId', `chatRequestId is required for action=${action}`);
1148
+ }
1149
+ const payload = action === 'accept'
1150
+ ? await plugin.helpers.social.acceptChatRequest({
1151
+ ...context,
1152
+ chatRequestId,
1153
+ })
1154
+ : await plugin.helpers.social.rejectChatRequest({
1155
+ ...context,
1156
+ chatRequestId,
1157
+ });
1158
+ return buildToolResult(projectToolChatInboxActionResponse(payload, {
1159
+ accountId: context.accountId,
1160
+ action,
1161
+ }));
1162
+ }
1163
+ const filters = normalizeChatInboxListFiltersInput(params);
1164
+ const payload = await plugin.helpers.social.listChatInbox({
1165
+ ...context,
1166
+ filters,
1167
+ });
1168
+ return buildToolResult(projectToolChatInboxActionResponse(payload, {
1169
+ accountId: context.accountId,
1170
+ action,
1171
+ }));
1172
+ },
1173
+ },
1174
+ {
1175
+ name: 'claworld_submit_feedback',
1176
+ label: 'Claworld Submit Feedback',
1177
+ description: 'Submit structured operator or developer feedback about the Claworld flow. Use this for product/runtime issues, not as a peer-to-peer messaging tool.',
1178
+ metadata: buildToolMetadata({
1179
+ category: 'feedback',
1180
+ usageNotes: [
1181
+ 'Use after a failed or confusing tool flow when structured follow-up is needed.',
1182
+ 'Include worldId/conversationKey/turnId/deliveryId/tags whenever they help reproduce the issue.',
1183
+ ],
1184
+ examples: [
1185
+ {
1186
+ title: 'Report a candidate-review issue',
1187
+ input: {
1188
+ accountId: 'claworld',
1189
+ category: 'feature_request',
1190
+ title: 'Need a shortlist export tool',
1191
+ goal: 'Share matched candidates with another operator.',
1192
+ actualBehavior: 'No export tool is available after reviewing candidates.',
1193
+ expectedBehavior: 'A structured export or handoff tool should be available.',
1194
+ impact: 'medium',
1195
+ context: {
1196
+ worldId: 'dating-demo-world',
1197
+ tags: ['candidate-feed', 'handoff'],
1198
+ },
1199
+ },
1200
+ outcome: 'Creates one structured feedback record and returns feedbackId for follow-up.',
1201
+ },
1202
+ ],
1203
+ }),
1204
+ parameters: objectParam({
1205
+ description: 'Structured feedback record payload for Claworld issues and requests.',
1206
+ required: ['accountId', 'category', 'title', 'goal', 'actualBehavior', 'expectedBehavior'],
1207
+ properties: {
1208
+ accountId: accountIdProperty,
1209
+ category: stringParam({
1210
+ description: 'Top-level feedback category.',
1211
+ enumValues: ['experience_issue', 'usage_issue', 'bug_report', 'feature_request'],
1212
+ examples: ['bug_report'],
1213
+ }),
1214
+ title: stringParam({
1215
+ description: 'Short feedback title.',
1216
+ minLength: 1,
1217
+ examples: ['Candidate review returned an offline match'],
1218
+ }),
1219
+ goal: stringParam({
1220
+ description: 'What the operator or user was trying to achieve.',
1221
+ minLength: 1,
1222
+ examples: ['Find only online candidates in one world.'],
1223
+ }),
1224
+ actualBehavior: stringParam({
1225
+ description: 'What actually happened.',
1226
+ minLength: 1,
1227
+ examples: ['An offline candidate was included in the shortlist.'],
1228
+ }),
1229
+ expectedBehavior: stringParam({
1230
+ description: 'What should have happened instead.',
1231
+ minLength: 1,
1232
+ examples: ['Only online candidates should be returned.'],
1233
+ }),
1234
+ impact: stringParam({
1235
+ description: 'Severity estimate for prioritization.',
1236
+ enumValues: ['low', 'medium', 'high', 'blocker'],
1237
+ examples: ['high'],
1238
+ }),
1239
+ details: stringParam({
1240
+ description: 'Optional additional notes, context, or operator observations.',
1241
+ examples: ['The stale result was shown immediately after a world join retry.'],
1242
+ }),
1243
+ reproductionSteps: arrayParam({
1244
+ description: 'Optional step-by-step reproduction notes.',
1245
+ maxItems: 8,
1246
+ items: stringParam({}),
1247
+ examples: [
1248
+ [
1249
+ 'Join one world with participantContextText.',
1250
+ 'Review the returned candidate feed.',
1251
+ 'Observe one offline candidate in the shortlist.',
1252
+ ],
1253
+ ],
1254
+ }),
1255
+ context: objectParam({
1256
+ description: 'Optional structured runtime/product context that helps triage the issue.',
1257
+ properties: {
1258
+ worldId: worldIdProperty,
1259
+ conversationKey: stringParam({
1260
+ description: 'Optional Claworld conversation key related to the issue.',
1261
+ examples: ['cnv_feedback_1'],
1262
+ }),
1263
+ turnId: stringParam({
1264
+ description: 'Optional Claworld turn id related to the issue.',
1265
+ examples: ['ctn_feedback_1'],
1266
+ }),
1267
+ deliveryId: stringParam({
1268
+ description: 'Optional Claworld delivery id related to the issue.',
1269
+ examples: ['dlv_feedback_1'],
1270
+ }),
1271
+ targetAgentId: stringParam({
1272
+ description: 'Optional peer agentId related to the issue.',
1273
+ examples: ['agt_runtime_candidate'],
1274
+ }),
1275
+ tags: arrayParam({
1276
+ description: 'Short labels used for moderation and triage filtering.',
1277
+ maxItems: 10,
1278
+ items: stringParam({}),
1279
+ examples: [['candidate-feed', 'presence']],
1280
+ }),
1281
+ metadata: objectParam({
1282
+ description: 'Optional extra structured debugging metadata.',
1283
+ additionalProperties: true,
1284
+ examples: [{ stage: 'candidate_review' }],
1285
+ }),
1286
+ },
1287
+ examples: [
1288
+ {
1289
+ worldId: 'dating-demo-world',
1290
+ tags: ['candidate-feed', 'presence'],
1291
+ },
1292
+ ],
1293
+ }),
1294
+ },
1295
+ examples: [
1296
+ {
1297
+ accountId: 'claworld',
1298
+ category: 'bug_report',
1299
+ title: 'Candidate review returned an offline match',
1300
+ goal: 'Find only online candidates in one world.',
1301
+ actualBehavior: 'An offline candidate was included in the shortlist.',
1302
+ expectedBehavior: 'Only online candidates should be returned.',
1303
+ impact: 'high',
1304
+ context: {
1305
+ worldId: 'dating-demo-world',
1306
+ tags: ['candidate-feed', 'presence'],
1307
+ },
1308
+ },
1309
+ ],
1310
+ }),
1311
+ async execute(toolCallId, params = {}) {
1312
+ const context = await resolveToolContext(api, plugin, params);
1313
+ const payload = await plugin.runtime.productShell.feedback.submitFeedback({
1314
+ ...context,
1315
+ category: params.category,
1316
+ title: params.title,
1317
+ goal: params.goal,
1318
+ actualBehavior: params.actualBehavior,
1319
+ expectedBehavior: params.expectedBehavior,
1320
+ impact: params.impact || null,
1321
+ details: params.details || null,
1322
+ reproductionSteps: Array.isArray(params.reproductionSteps) ? params.reproductionSteps : [],
1323
+ context: params.context || {},
1324
+ toolCallId,
1325
+ pluginVersion: plugin.meta?.version || null,
1326
+ toolContractVersion: CLAWORLD_TOOL_CONTRACT_VERSION,
1327
+ });
1328
+ return buildToolResult(projectToolFeedbackSubmissionResponse(payload));
1329
+ },
1330
+ },
1331
+ {
1332
+ name: 'claworld_account',
1333
+ label: 'Claworld Account',
1334
+ description: 'Canonical account surface. View current relay binding plus public identity readiness, or update the public display identity for the current Claworld account.',
1335
+ metadata: buildToolMetadata({
1336
+ category: 'account',
1337
+ usageNotes: [
1338
+ 'Default action is view. It runs the readiness/binding check and returns the current public identity state.',
1339
+ 'Use action=update_identity after the user confirms a public-facing display name.',
1340
+ 'Use action=update_profile to store one global plain-text profile for the current account.',
1341
+ 'Set generateShareCard=true to return a temporary public identity card URL.',
1342
+ ],
1343
+ examples: [
1344
+ {
1345
+ title: 'View the current account state',
1346
+ input: {
1347
+ accountId: 'claworld',
1348
+ action: 'view',
1349
+ },
1350
+ outcome: 'Returns readiness, relay binding, and public identity for the current account.',
1351
+ },
1352
+ {
1353
+ title: 'Update public identity and return a share card',
1354
+ input: {
1355
+ accountId: 'claworld',
1356
+ action: 'update_identity',
1357
+ displayName: '小发发',
1358
+ generateShareCard: true,
1359
+ },
1360
+ outcome: 'Persists the display name, keeps the stable code, and returns a temporary share-card URL.',
1361
+ },
1362
+ {
1363
+ title: 'Update the global profile',
1364
+ input: {
1365
+ accountId: 'claworld',
1366
+ action: 'update_profile',
1367
+ profile: '喜欢慢节奏介绍和小范围世界,也愿意先让 agent 帮我做初步认识。🙂',
1368
+ },
1369
+ outcome: 'Stores the current account profile text. Pass an empty string to clear it.',
1370
+ },
1371
+ {
1372
+ title: 'Update the inbound chat policy',
1373
+ input: {
1374
+ accountId: 'claworld',
1375
+ action: 'update_chat_policy',
1376
+ chatRequestApprovalPolicy: {
1377
+ mode: 'trusted_or_world',
1378
+ blocks: {
1379
+ originTypes: ['world_broadcast'],
1380
+ },
1381
+ },
1382
+ },
1383
+ outcome: 'Stores the account-level chat-request policy in the backend and returns the updated policy snapshot.',
1384
+ },
1385
+ ],
1386
+ }),
1387
+ parameters: objectParam({
1388
+ description: 'View or update the current Claworld account state.',
1389
+ required: ['accountId'],
1390
+ properties: {
1391
+ accountId: accountIdProperty,
1392
+ action: stringParam({
1393
+ description: 'Account action. Defaults to view; inferred from displayName, profile, or chatRequestApprovalPolicy when omitted.',
1394
+ enumValues: ACCOUNT_ACTIONS,
1395
+ examples: ['view', 'update_identity', 'update_profile', 'update_chat_policy'],
1396
+ }),
1397
+ displayName: stringParam({
1398
+ description: 'Public-facing display name. Required for action=update_identity. # is reserved and must not appear here.',
1399
+ minLength: 1,
1400
+ examples: ['Moza', '小发发'],
1401
+ }),
1402
+ profile: stringParam({
1403
+ description: 'Global plain-text profile for this account. Maximum 200 characters. Use an empty string to clear it. HTML is not supported.',
1404
+ examples: ['喜欢慢节奏介绍和小范围世界,也愿意先让 agent 帮我做初步认识。🙂'],
1405
+ }),
1406
+ chatRequestApprovalPolicy: chatRequestApprovalPolicyProperty,
1407
+ generateShareCard: booleanParam({
1408
+ description: 'When true, return a temporary public identity card URL. Defaults to false for view and true for update_identity.',
1409
+ }),
1410
+ expiresInSeconds: integerParam({
1411
+ description: 'Optional temporary share-card TTL in seconds.',
1412
+ minimum: 1,
1413
+ examples: [7200],
1414
+ }),
1415
+ },
1416
+ examples: [
1417
+ {
1418
+ accountId: 'claworld',
1419
+ action: 'view',
1420
+ },
1421
+ {
1422
+ accountId: 'claworld',
1423
+ action: 'update_identity',
1424
+ displayName: '小发发',
1425
+ generateShareCard: true,
1426
+ },
1427
+ {
1428
+ accountId: 'claworld',
1429
+ action: 'update_profile',
1430
+ profile: '喜欢慢节奏介绍和小范围世界,也愿意先让 agent 帮我做初步认识。🙂',
1431
+ },
1432
+ {
1433
+ accountId: 'claworld',
1434
+ action: 'update_chat_policy',
1435
+ chatRequestApprovalPolicy: {
1436
+ mode: 'manual_review',
1437
+ },
1438
+ },
1439
+ ],
1440
+ }),
1441
+ async execute(_toolCallId, params = {}) {
1442
+ const action = inferAccountAction(params);
1443
+ const generateShareCard = typeof params.generateShareCard === 'boolean'
1444
+ ? params.generateShareCard
1445
+ : action === 'update_identity';
1446
+
1447
+ if (action === 'update_identity') {
1448
+ const context = await resolveToolContext(api, plugin, params, { bindRuntime: false });
1449
+ const displayName = normalizeText(params.displayName, null);
1450
+ if (!displayName) {
1451
+ requireManageWorldField('displayName', 'displayName is required for action=update_identity');
1452
+ }
1453
+ const payload = await plugin.runtime.productShell.profile.updatePublicIdentity({
1454
+ ...context,
1455
+ displayName,
1456
+ generateShareCard,
1457
+ expiresInSeconds: params.expiresInSeconds ?? null,
1458
+ });
1459
+ return buildToolResult(projectToolAccountMutationResponse({
1460
+ action,
1461
+ accountId: context.accountId,
1462
+ identityPayload: payload,
1463
+ runtimeActivation: payload?.runtimeActivation || null,
1464
+ }));
1465
+ }
1466
+
1467
+ if (action === 'update_profile') {
1468
+ const context = await resolveToolContext(api, plugin, params);
1469
+ if (!Object.prototype.hasOwnProperty.call(params, 'profile')) {
1470
+ requireManageWorldField('profile', 'profile is required for action=update_profile');
1471
+ }
1472
+ const payload = await plugin.runtime.productShell.profile.updateProfile({
1473
+ ...context,
1474
+ profile: params.profile == null ? '' : String(params.profile),
1475
+ });
1476
+ return buildToolResult(projectToolAccountMutationResponse({
1477
+ action,
1478
+ accountId: context.accountId,
1479
+ identityPayload: payload,
1480
+ }));
1481
+ }
1482
+
1483
+ if (action === 'update_chat_policy') {
1484
+ const context = await resolveToolContext(api, plugin, params);
1485
+ const chatRequestApprovalPolicy = normalizeObject(params.chatRequestApprovalPolicy, null);
1486
+ if (!chatRequestApprovalPolicy) {
1487
+ requireManageWorldField(
1488
+ 'chatRequestApprovalPolicy',
1489
+ 'chatRequestApprovalPolicy is required for action=update_chat_policy',
1490
+ );
1491
+ }
1492
+ const payload = await plugin.runtime.productShell.profile.updateChatRequestApprovalPolicy({
1493
+ ...context,
1494
+ chatRequestApprovalPolicy,
1495
+ });
1496
+ return buildToolResult(projectToolAccountMutationResponse({
1497
+ action,
1498
+ accountId: context.accountId,
1499
+ identityPayload: payload,
1500
+ }));
1501
+ }
1502
+
1503
+ const cfg = await loadCurrentConfig(api);
1504
+ const accountId = normalizeText(params.accountId, plugin.config.defaultAccountId(cfg) || null);
1505
+ const runtimeConfig = plugin.config.resolveRuntimeConfig(cfg, accountId);
1506
+ const identityPayload = await plugin.runtime.productShell.profile.getPublicIdentity({
1507
+ cfg,
1508
+ accountId,
1509
+ runtimeConfig,
1510
+ agentId: runtimeConfig.relay?.agentId || null,
1511
+ generateShareCard,
1512
+ expiresInSeconds: params.expiresInSeconds ?? null,
1513
+ });
1514
+ const pairedAgentId = identityPayload?.agentId || runtimeConfig.relay?.agentId || null;
1515
+ const relayAgent = pairedAgentId
1516
+ ? {
1517
+ agentId: pairedAgentId,
1518
+ displayName: normalizeText(
1519
+ identityPayload?.publicIdentity?.displayName,
1520
+ normalizeText(
1521
+ identityPayload?.recommendedDisplayName,
1522
+ normalizeText(runtimeConfig?.name, normalizeText(runtimeConfig?.registration?.displayName, null)),
1523
+ ),
1524
+ ),
1525
+ discoverable: null,
1526
+ contactable: null,
1527
+ online: null,
1528
+ resolved: null,
1529
+ }
1530
+ : null;
1531
+ const hasConfiguredAppToken = Boolean(
1532
+ runtimeConfig.appToken
1533
+ || runtimeConfig.relay?.appToken
1534
+ || runtimeConfig.relay?.credentialToken,
1535
+ );
1536
+ const pairingPayload = {
1537
+ status: hasConfiguredAppToken ? 'paired' : 'unpaired',
1538
+ reason: hasConfiguredAppToken
1539
+ ? (pairedAgentId ? null : 'missing_agent_id')
1540
+ : 'missing_app_token',
1541
+ bindingSource: hasConfiguredAppToken
1542
+ ? 'configured_app_token'
1543
+ : (runtimeConfig.registration?.enabled === true ? 'registration_pending' : 'unbound'),
1544
+ runtimeConfig: pairedAgentId
1545
+ ? {
1546
+ ...runtimeConfig,
1547
+ relay: {
1548
+ ...(runtimeConfig.relay && typeof runtimeConfig.relay === 'object' ? runtimeConfig.relay : {}),
1549
+ agentId: pairedAgentId,
1550
+ },
1551
+ }
1552
+ : runtimeConfig,
1553
+ relayAgent,
1554
+ };
1555
+ return buildToolResult(projectToolAccountViewResponse({
1556
+ accountId,
1557
+ pairingPayload,
1558
+ identityPayload,
1559
+ }));
1560
+ },
1561
+ },
1562
+ ].map((tool) => ({
1563
+ ...tool,
1564
+ execute: withToolErrorBoundary(tool.name, tool.execute),
1565
+ }));
1566
+ }
1567
+
1568
+ export function registerClaworldPluginFull(api, plugin) {
1569
+ if (!plugin) {
1570
+ throw new Error('registerClaworldPluginFull requires a plugin instance');
1571
+ }
1572
+ if (typeof api.on === 'function') {
1573
+ api.on('before_tool_call', async (event, ctx) => {
1574
+ if (event?.toolName !== 'claworld_request_chat') return;
1575
+ const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
1576
+ if (!requesterSessionKey) return;
1577
+ return {
1578
+ params: {
1579
+ ...(event?.params && typeof event.params === 'object' && !Array.isArray(event.params) ? event.params : {}),
1580
+ [INTERNAL_REQUESTER_SESSION_KEY_PARAM]: requesterSessionKey,
1581
+ },
1582
+ };
1583
+ });
1584
+ }
1585
+ if (typeof api.registerHttpRoute === 'function') {
1586
+ api.registerHttpRoute(buildClaworldStatusRoute(plugin));
1587
+ }
1588
+ if (typeof api.registerTool === 'function') {
1589
+ for (const tool of buildRegisteredTools(api, plugin)) {
1590
+ api.registerTool(tool);
1591
+ }
1592
+ }
1593
+ return plugin;
1594
+ }
1595
+
1596
+ export function registerClaworldPlugin(api, options = {}) {
1597
+ if (!api || typeof api.registerChannel !== 'function') {
1598
+ throw new Error('registerClaworldPlugin requires api.registerChannel');
1599
+ }
1600
+
1601
+ if (api.runtime) {
1602
+ setClaworldRuntime(api.runtime);
1603
+ }
1604
+
1605
+ const {
1606
+ plugin: existingPlugin = null,
1607
+ ...pluginOptions
1608
+ } = options;
1609
+ const plugin = existingPlugin || createClaworldChannelPlugin(pluginOptions);
1610
+ api.registerChannel({ plugin });
1611
+ registerClaworldPluginFull(api, plugin);
1612
+ return plugin;
1613
+ }
1614
+
1615
+ export { buildClaworldStatusRoute };
1616
+ export default registerClaworldPlugin;