@xfxstudio/claworld 0.1.5 → 0.2.1

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 (54) hide show
  1. package/README.md +12 -29
  2. package/openclaw.plugin.json +5 -29
  3. package/package.json +4 -12
  4. package/skills/claworld-help/SKILL.md +50 -182
  5. package/skills/claworld-join-and-chat/SKILL.md +78 -288
  6. package/skills/claworld-manage-worlds/SKILL.md +71 -288
  7. package/src/lib/chat-request.js +347 -0
  8. package/src/lib/{accepted-chat-kickoff.js → relay/kickoff-text.js} +67 -26
  9. package/src/openclaw/index.js +0 -5
  10. package/src/openclaw/installer/cli.js +18 -9
  11. package/src/openclaw/installer/core.js +12 -6
  12. package/src/openclaw/installer/doctor.js +69 -31
  13. package/src/openclaw/installer/workspace-contract.js +33 -9
  14. package/src/openclaw/plugin/claworld-channel-plugin.js +118 -623
  15. package/src/openclaw/plugin/config-schema.js +3 -15
  16. package/src/openclaw/plugin/managed-config.js +98 -47
  17. package/src/openclaw/plugin/onboarding.js +7 -3
  18. package/src/openclaw/plugin/register.js +37 -336
  19. package/src/openclaw/plugin/relay-client.js +111 -101
  20. package/src/openclaw/protocol/relay-event-protocol.js +34 -22
  21. package/src/openclaw/runtime/canonical-result-builder.js +15 -5
  22. package/src/openclaw/runtime/demo-session-bootstrap.js +0 -4
  23. package/src/openclaw/runtime/feedback-helper.js +3 -2
  24. package/src/openclaw/runtime/inbound-session-router.js +28 -20
  25. package/src/openclaw/runtime/outbound-session-bridge.js +21 -9
  26. package/src/openclaw/runtime/product-shell-helper.js +43 -636
  27. package/src/openclaw/runtime/runtime-path.js +2 -2
  28. package/src/openclaw/runtime/system-message-orchestrator.js +1 -1
  29. package/src/openclaw/runtime/tool-contracts.js +33 -258
  30. package/src/openclaw/runtime/world-moderation-helper.js +11 -65
  31. package/src/product-shell/catalog/default-world-catalog.js +9 -27
  32. package/src/product-shell/contracts/candidate-feed.js +26 -1
  33. package/src/product-shell/contracts/chat-request-approval-policy.js +4 -4
  34. package/src/product-shell/contracts/world-manifest.js +115 -160
  35. package/src/product-shell/contracts/world-orchestration.js +47 -322
  36. package/src/product-shell/feedback/feedback-routes.js +4 -3
  37. package/src/product-shell/feedback/feedback-service.js +11 -8
  38. package/src/product-shell/index.js +5 -6
  39. package/src/product-shell/membership/membership-service.js +125 -147
  40. package/src/product-shell/onboarding/onboarding-service.js +2 -2
  41. package/src/product-shell/orchestration/world-conversation-orchestrator.js +30 -0
  42. package/src/product-shell/orchestration/world-conversation-text.js +231 -0
  43. package/src/product-shell/results/result-service.js +9 -3
  44. package/src/product-shell/search/search-service.js +28 -1
  45. package/src/product-shell/social/chat-request-routes.js +0 -1
  46. package/src/product-shell/social/chat-request-service.js +1 -102
  47. package/src/product-shell/worlds/world-admin-service.js +85 -276
  48. package/src/product-shell/worlds/world-authorization.js +3 -5
  49. package/src/product-shell/worlds/world-routes.js +8 -38
  50. package/src/product-shell/worlds/world-service.js +3 -3
  51. package/src/product-shell/worlds/world-text.js +77 -0
  52. package/src/lib/runtime-guidance.js +0 -457
  53. package/src/openclaw/runtime/world-session-startup.js +0 -1
  54. package/src/product-shell/orchestration/session-orchestrator.js +0 -38
@@ -1,21 +1,10 @@
1
- import {
2
- buildWorldJoinOutcomeOrchestration,
3
- buildWorldProfileCollectionFlow,
4
- } from '../contracts/world-orchestration.js';
1
+ import { buildResolvedWorldJoinOrchestration } from '../contracts/world-orchestration.js';
2
+ import { buildParticipantContextText } from '../worlds/world-text.js';
5
3
 
6
- function isEmptyValue(value) {
7
- if (value == null) return true;
8
- if (typeof value === 'string') return value.trim() === '';
9
- if (Array.isArray(value)) return value.length === 0;
10
- return false;
11
- }
12
-
13
- function projectMissingField(field = {}) {
14
- return {
15
- fieldId: field.fieldId,
16
- label: field.label,
17
- description: field.description,
18
- };
4
+ function normalizeText(value, fallback = null) {
5
+ if (value == null) return fallback;
6
+ const normalized = String(value).trim();
7
+ return normalized || fallback;
19
8
  }
20
9
 
21
10
  function createConfigurationError() {
@@ -49,67 +38,30 @@ function createInvalidJoinRequestError(fieldId, message = `${fieldId} is require
49
38
  return error;
50
39
  }
51
40
 
52
- function createMembershipNotEligibleError(joinCheck) {
53
- const error = new Error('membership_not_eligible');
54
- error.code = 'membership_not_eligible';
55
- error.status = 422;
56
- error.responseBody = {
57
- error: error.code,
58
- message: 'profile does not satisfy world join requirements',
59
- joinCheck,
60
- profileCollectionFlow: joinCheck.profileCollectionFlow || null,
61
- };
62
- return error;
63
- }
64
-
65
- function createJoinNotEligibleError(joinCheck) {
66
- const error = new Error('world_join_not_eligible');
67
- error.code = 'world_join_not_eligible';
68
- error.status = 422;
69
- error.responseBody = {
70
- error: error.code,
71
- message: 'profile does not satisfy world join requirements',
72
- status: 'needs_profile',
73
- membershipStatus: 'inactive',
74
- worldId: joinCheck.worldId,
75
- normalizedProfile: joinCheck.normalizedProfile,
76
- missingFields: joinCheck.missingFields,
77
- nextMissingField: joinCheck.nextMissingField,
78
- missingFieldGuidance: joinCheck.missingFieldGuidance,
79
- nextAction: 'retry_join_world_after_profile_update',
80
- };
81
- return error;
82
- }
83
-
84
41
  function normalizeAgentId(agentId) {
85
42
  const normalized = String(agentId || '').trim();
86
43
  return normalized || null;
87
44
  }
88
45
 
89
- function normalizeProfileSnapshot(...candidates) {
90
- for (const candidate of candidates) {
91
- if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
92
- return candidate;
93
- }
46
+ function normalizeProfileSnapshot(profileSnapshot = null, participantContextText = null) {
47
+ const base = profileSnapshot && typeof profileSnapshot === 'object' && !Array.isArray(profileSnapshot)
48
+ ? { ...profileSnapshot }
49
+ : {};
50
+ const normalizedParticipantContextText = normalizeText(
51
+ participantContextText,
52
+ normalizeText(base.participantContextText, null),
53
+ );
54
+ if (normalizedParticipantContextText) {
55
+ base.participantContextText = normalizedParticipantContextText;
94
56
  }
95
- return {};
57
+ return base;
96
58
  }
97
59
 
98
- function buildNextStageSummary(world) {
99
- const matchingSummary = world.matching.strategySummary
100
- || `The world uses ${world.matching.mode} matching to deliver candidate summaries before request_chat.`;
101
- const reviewSummary =
102
- 'Review the backend-authored candidate feed, choose a candidate, and create a world-scoped chat request.';
103
- const sessionSummary = `Matched agents then enter a ${world.sessionTemplate.mode} session with up to ${world.sessionTemplate.maxTurns} turns.`;
104
-
60
+ function buildNextStageSummary() {
105
61
  return {
106
- stage: 'matchmaking',
107
- summary: `${matchingSummary} ${reviewSummary} ${sessionSummary}`.trim(),
108
- matchingMode: world.matching.mode,
109
- matchingCadence: world.matching.cadence,
110
- sessionMode: world.sessionTemplate.mode,
111
- maxTurns: world.sessionTemplate.maxTurns,
112
- turnTimeoutMs: world.sessionTemplate.turnTimeoutMs,
62
+ stage: 'candidate_review',
63
+ summary:
64
+ 'Review the backend-authored candidate feed or use world search, then choose one target agent and create a world-scoped chat request.',
113
65
  };
114
66
  }
115
67
 
@@ -119,67 +71,86 @@ export function createMembershipService({ worldService, store = null } = {}) {
119
71
  return store;
120
72
  }
121
73
 
74
+ function resolveNormalizedParticipantContextText({ participantContextText = null, profileSnapshot = null, agent = null, world = null } = {}) {
75
+ return buildParticipantContextText({
76
+ world,
77
+ agent,
78
+ participantContextText,
79
+ profileSnapshot,
80
+ });
81
+ }
82
+
122
83
  return {
123
- evaluateJoin({ worldId, profile = {}, maxFieldsPerStep = 1 } = {}) {
84
+ evaluateJoin({ worldId, participantContextText = null } = {}) {
124
85
  const world = worldService.requireWorld(worldId);
125
- const normalizedProfile = profile && typeof profile === 'object' ? profile : {};
126
- const orderedMissingFields = world.joinSchema.requiredFields.filter((field) =>
127
- isEmptyValue(normalizedProfile[field.fieldId]),
128
- );
129
- const missingFields = orderedMissingFields.map((field) => projectMissingField(field));
130
- const nextMissingField = missingFields[0] || null;
86
+ const normalizedParticipantContextText = normalizeText(participantContextText, null);
87
+ const accepted = Boolean(normalizedParticipantContextText);
131
88
 
132
- const joinCheck = {
89
+ return {
133
90
  worldId: world.worldId,
134
- accepted: missingFields.length === 0,
135
- status: missingFields.length === 0 ? 'eligible' : 'needs_profile',
136
- missingFields,
137
- nextMissingField,
91
+ accepted,
92
+ status: accepted ? 'eligible' : 'needs_profile',
93
+ participantContextText: normalizedParticipantContextText,
94
+ missingFields: accepted
95
+ ? []
96
+ : [
97
+ {
98
+ fieldId: 'participantContextText',
99
+ label: 'Entry Profile',
100
+ description: 'A short text describing who you are in this world and what context you bring into it.',
101
+ },
102
+ ],
103
+ nextMissingField: accepted
104
+ ? null
105
+ : {
106
+ fieldId: 'participantContextText',
107
+ label: 'Entry Profile',
108
+ description: 'A short text describing who you are in this world and what context you bring into it.',
109
+ },
138
110
  missingFieldGuidance: {
139
- mode: nextMissingField ? 'ordered_required_fields' : 'complete',
140
- orderedMissingFields: missingFields,
141
- orderedMissingFieldIds: missingFields.map((field) => field.fieldId),
142
- nextMissingField,
143
- remainingRequiredFieldCount: missingFields.length,
111
+ mode: accepted ? 'complete' : 'single_text_field',
112
+ orderedMissingFields: accepted
113
+ ? []
114
+ : [
115
+ {
116
+ fieldId: 'participantContextText',
117
+ label: 'Entry Profile',
118
+ description: 'A short text describing who you are in this world and what context you bring into it.',
119
+ },
120
+ ],
121
+ orderedMissingFieldIds: accepted ? [] : ['participantContextText'],
122
+ nextMissingField: accepted
123
+ ? null
124
+ : {
125
+ fieldId: 'participantContextText',
126
+ label: 'Entry Profile',
127
+ description: 'A short text describing who you are in this world and what context you bring into it.',
128
+ },
129
+ remainingRequiredFieldCount: accepted ? 0 : 1,
144
130
  },
145
- normalizedProfile,
146
- nextAction:
147
- missingFields.length === 0 ? 'call_join_world' : 'retry_join_world_after_profile_update',
148
- };
149
-
150
- const worldDetail = worldService.describeWorldDetail(world.worldId);
151
- return {
152
- ...joinCheck,
153
- profileCollectionFlow: buildWorldProfileCollectionFlow({
154
- worldDetail,
155
- joinCheck,
156
- profile: normalizedProfile,
157
- maxFieldsPerStep,
158
- }),
131
+ nextAction: accepted ? 'call_join_world' : 'retry_join_world_after_profile_update',
159
132
  };
160
133
  },
161
- async createMembership({ worldId, agentId, profileSnapshot } = {}) {
162
- worldService.requireWorld(worldId);
134
+
135
+ async createMembership({ worldId, agentId, participantContextText } = {}) {
136
+ const world = worldService.requireWorld(worldId);
163
137
  const membershipStore = assertStore();
164
138
  const normalizedAgentId = normalizeAgentId(agentId);
165
- if (!normalizedAgentId) {
166
- throw createInvalidJoinRequestError('agentId');
167
- }
139
+ if (!normalizedAgentId) throw createInvalidJoinRequestError('agentId');
168
140
 
169
141
  const agent = membershipStore.getAgent(normalizedAgentId);
142
+ if (!agent) throw createAgentNotFoundError(normalizedAgentId);
170
143
 
171
- if (!agent) {
172
- throw createAgentNotFoundError(normalizedAgentId);
173
- }
174
-
175
- const normalizedProfileSnapshot = normalizeProfileSnapshot(profileSnapshot, agent.profile);
176
- const joinCheck = this.evaluateJoin({
177
- worldId,
178
- profile: normalizedProfileSnapshot,
144
+ const normalizedParticipantContextText = resolveNormalizedParticipantContextText({
145
+ world,
146
+ agent,
147
+ participantContextText,
179
148
  });
180
-
181
- if (!joinCheck.accepted) {
182
- throw createMembershipNotEligibleError(joinCheck);
149
+ if (!normalizedParticipantContextText) {
150
+ throw createInvalidJoinRequestError(
151
+ 'participantContextText',
152
+ 'participantContextText is required',
153
+ );
183
154
  }
184
155
 
185
156
  const existingMembership = membershipStore.listMemberships({
@@ -195,34 +166,37 @@ export function createMembershipService({ worldService, store = null } = {}) {
195
166
  worldId,
196
167
  agentId: normalizedAgentId,
197
168
  status: 'joined',
198
- profileSnapshot: joinCheck.normalizedProfile,
169
+ profileSnapshot: normalizeProfileSnapshot(null, normalizedParticipantContextText),
170
+ participantContextText: normalizedParticipantContextText,
199
171
  });
200
172
 
201
173
  return { membership, created: true };
202
174
  },
203
- async joinWorld({ worldId, agentId, profile, profileSnapshot, maxFieldsPerStep = 1 } = {}) {
175
+
176
+ async joinWorld({
177
+ worldId,
178
+ agentId,
179
+ participantContextText,
180
+ } = {}) {
204
181
  const world = worldService.requireWorld(worldId);
205
182
  const membershipStore = assertStore();
206
183
  const normalizedAgentId = normalizeAgentId(agentId);
207
184
 
208
- if (!normalizedAgentId) {
209
- throw createInvalidJoinRequestError('agentId');
210
- }
185
+ if (!normalizedAgentId) throw createInvalidJoinRequestError('agentId');
211
186
 
212
187
  const agent = membershipStore.getAgent(normalizedAgentId);
213
- if (!agent) {
214
- throw createAgentNotFoundError(normalizedAgentId);
215
- }
188
+ if (!agent) throw createAgentNotFoundError(normalizedAgentId);
216
189
 
217
- const effectiveProfile = normalizeProfileSnapshot(profile, profileSnapshot, agent.profile);
218
- const joinCheck = this.evaluateJoin({
219
- worldId: world.worldId,
220
- profile: effectiveProfile,
221
- maxFieldsPerStep,
190
+ const normalizedParticipantContextText = resolveNormalizedParticipantContextText({
191
+ world,
192
+ agent,
193
+ participantContextText,
222
194
  });
223
-
224
- if (!joinCheck.accepted) {
225
- throw createJoinNotEligibleError(joinCheck);
195
+ if (!normalizedParticipantContextText) {
196
+ throw createInvalidJoinRequestError(
197
+ 'participantContextText',
198
+ 'participantContextText is required',
199
+ );
226
200
  }
227
201
 
228
202
  const existingMembership = membershipStore.listMemberships({
@@ -233,49 +207,53 @@ export function createMembershipService({ worldService, store = null } = {}) {
233
207
  const membership = existingMembership
234
208
  ? await membershipStore.updateMembership(existingMembership.membershipId, {
235
209
  status: 'active',
236
- profileSnapshot: joinCheck.normalizedProfile,
210
+ profileSnapshot: normalizeProfileSnapshot(existingMembership.profileSnapshot, normalizedParticipantContextText),
211
+ participantContextText: normalizedParticipantContextText,
237
212
  })
238
213
  : await membershipStore.createMembership({
239
214
  worldId: world.worldId,
240
215
  agentId: normalizedAgentId,
241
216
  status: 'active',
242
- profileSnapshot: joinCheck.normalizedProfile,
217
+ profileSnapshot: normalizeProfileSnapshot(null, normalizedParticipantContextText),
218
+ participantContextText: normalizedParticipantContextText,
243
219
  });
244
220
 
245
- return {
221
+ const joinResult = {
246
222
  status: 'joined',
247
223
  worldId: world.worldId,
248
224
  membership,
249
225
  membershipStatus: membership.status,
250
226
  created: !existingMembership,
251
- normalizedProfile: joinCheck.normalizedProfile,
227
+ participantContextText: normalizedParticipantContextText,
252
228
  nextAction: 'review_candidate_feed',
253
- nextStageSummary: buildNextStageSummary(world),
254
- orchestration: buildWorldJoinOutcomeOrchestration({
255
- worldDetail: worldService.describeWorldDetail(world.worldId),
256
- joinResult: {
257
- membershipStatus: membership.status,
258
- membership,
259
- normalizedProfile: joinCheck.normalizedProfile,
260
- nextAction: 'review_candidate_feed',
261
- nextStageSummary: buildNextStageSummary(world),
262
- },
263
- }),
229
+ nextStageSummary: buildNextStageSummary(),
230
+ };
231
+
232
+ return {
233
+ ...joinResult,
234
+ orchestration: buildResolvedWorldJoinOrchestration({
235
+ joinResult,
236
+ candidateDelivery: null,
237
+ }) || null,
264
238
  };
265
239
  },
240
+
266
241
  getMembership({ worldId, agentId, includeDisabled = false } = {}) {
267
242
  const normalizedAgentId = normalizeAgentId(agentId);
268
243
  if (!normalizedAgentId) return null;
269
244
  worldService.requireWorld(worldId, { includeDisabled });
270
245
  return assertStore().listMemberships({ worldId, agentId: normalizedAgentId })[0] || null;
271
246
  },
247
+
272
248
  listMemberships({ worldId, agentId = null, status = null, includeDisabled = false } = {}) {
273
249
  worldService.requireWorld(worldId, { includeDisabled });
274
250
  return assertStore().listMemberships({ worldId, agentId, status });
275
251
  },
252
+
276
253
  listMembershipsAcrossWorlds({ agentId = null, status = null } = {}) {
277
254
  return assertStore().listMemberships({ agentId, status });
278
255
  },
256
+
279
257
  countMemberships({ worldId, agentId = null, status = null, includeDisabled = false } = {}) {
280
258
  worldService.requireWorld(worldId, { includeDisabled });
281
259
  return assertStore().countMemberships({ worldId, agentId, status });
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  const DEFAULT_INSTALL_CHANNEL_ID = 'claworld';
11
11
  const DEFAULT_INSTALL_ACCOUNT_ID = 'claworld';
12
- const DEFAULT_INSTALL_LOCAL_AGENT_ID = 'claworld';
12
+ const DEFAULT_INSTALL_LOCAL_AGENT_ID = 'main';
13
13
  const DEFAULT_ACTIVATION_DISPLAY_NAME = 'Claworld Agent';
14
14
 
15
15
  function normalizeText(value, fallback = null) {
@@ -174,7 +174,7 @@ export function createOnboardingService({ worldService, store = null } = {}) {
174
174
  `run ${CLAWORLD_INSTALLER_COMMAND}`,
175
175
  'installer validates OpenClaw availability and minimum host version',
176
176
  'installer verifies or installs the claworld OpenClaw plugin package',
177
- 'installer writes or refreshes the managed claworld channel config',
177
+ 'installer writes or refreshes the managed claworld channel config and binds it to the local main agent by default',
178
178
  'installer calls POST /v1/onboarding/activate to obtain agentId and appToken when reuse is not possible',
179
179
  'installer persists the returned appToken into the managed claworld account config',
180
180
  'installer reloads or starts the runtime and verifies the relay binding',
@@ -0,0 +1,30 @@
1
+ import { createSystemMessageOrchestrator } from './world-conversation-text.js';
2
+
3
+ export function createWorldConversationOrchestrator({
4
+ worldService,
5
+ resultService,
6
+ systemMessages = createSystemMessageOrchestrator(),
7
+ } = {}) {
8
+ return {
9
+ previewConversation({ worldId, conversationKey = 'cnv_preview' } = {}) {
10
+ const world = worldService.requireWorld(worldId);
11
+ const openingPlan = systemMessages.planMessages({
12
+ conversationId: conversationKey,
13
+ trigger: 'conversation_started',
14
+ worldRules: world.conversationTemplate.worldRules,
15
+ });
16
+ const convergencePlan = [];
17
+
18
+ return {
19
+ worldId: world.worldId,
20
+ conversationTemplate: {
21
+ mode: world.conversationTemplate.mode,
22
+ },
23
+ openingPlan,
24
+ convergencePlan,
25
+ resultPreview: resultService.previewConversation({ world, conversationKey }),
26
+ status: 'preview_ready',
27
+ };
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,231 @@
1
+ const DEFAULT_TEMPLATE_REFS = {
2
+ opening: 'world.conversation.opening',
3
+ convergence: 'world.conversation.convergence',
4
+ stateChanged: 'world.conversation.state_changed',
5
+ };
6
+
7
+ function normalizeText(value, fallback = null) {
8
+ if (value == null) return fallback;
9
+ const normalized = String(value).trim();
10
+ return normalized || fallback;
11
+ }
12
+
13
+ function normalizeInteger(value, fallback = null) {
14
+ const normalized = Number(value);
15
+ if (!Number.isFinite(normalized)) return fallback;
16
+ return Math.trunc(normalized);
17
+ }
18
+
19
+ function normalizePositiveInteger(value, fallback = null) {
20
+ const normalized = normalizeInteger(value, fallback);
21
+ if (normalized == null || normalized <= 0) return fallback;
22
+ return normalized;
23
+ }
24
+
25
+ function normalizeTurnRule(rule = {}, index = 0) {
26
+ return {
27
+ id: rule.id || `turn_rule_${index + 1}`,
28
+ trigger: rule.trigger || 'turn_threshold',
29
+ atTurn: Number.isFinite(Number(rule.atTurn)) ? Math.max(0, Number(rule.atTurn)) : null,
30
+ visibility: rule.visibility || 'both',
31
+ role: rule.role || 'system',
32
+ templateRef: rule.templateRef || `world.turn.rule.${index + 1}`,
33
+ text: rule.text || null,
34
+ once: rule.once !== false,
35
+ };
36
+ }
37
+
38
+ function buildMessage({
39
+ conversationId = null,
40
+ trigger,
41
+ role = 'system',
42
+ visibility = 'both',
43
+ templateRef = null,
44
+ text = null,
45
+ metadata = {},
46
+ }) {
47
+ return {
48
+ conversationId: normalizeText(conversationId, null),
49
+ trigger,
50
+ role,
51
+ visibility,
52
+ templateRef,
53
+ text,
54
+ metadata,
55
+ };
56
+ }
57
+
58
+ function formatConversationOverview(detail = {}) {
59
+ const conversationOverview = detail.conversationOverview && typeof detail.conversationOverview === 'object'
60
+ ? detail.conversationOverview
61
+ : {};
62
+ const mode = normalizeText(detail.conversationMode || conversationOverview.mode, null);
63
+ const parts = [];
64
+
65
+ if (mode) parts.push(`${mode} mode`);
66
+
67
+ return parts.length > 0 ? parts.join(', ') : null;
68
+ }
69
+
70
+ export function buildWorldConversationContextEvent(detail = {}) {
71
+ const worldId = normalizeText(detail.worldId || detail.world?.worldId, null);
72
+ if (!worldId) return null;
73
+ const worldContextText = normalizeText(
74
+ detail.world?.worldContextText,
75
+ normalizeText(detail.worldContextText, null),
76
+ );
77
+ if (worldContextText) return worldContextText;
78
+
79
+ const displayName = normalizeText(detail.displayName || detail.world?.displayName || detail.worldDisplayName, worldId);
80
+ const summary = normalizeText(detail.summary || detail.world?.summary, null);
81
+ const sessionSummary = formatConversationOverview(detail);
82
+ const conversationOverview = detail.conversationOverview && typeof detail.conversationOverview === 'object'
83
+ ? detail.conversationOverview
84
+ : {};
85
+ const openingText = normalizeText(conversationOverview.openingText, null);
86
+ const convergenceText = normalizeText(conversationOverview.convergence?.text, null);
87
+ const interactionRules = normalizeText(detail.interactionRules, null);
88
+ const prohibitedRules = normalizeText(detail.prohibitedRules, null);
89
+ const ratingRules = normalizeText(detail.ratingRules, null);
90
+
91
+ const lines = [
92
+ 'Internal Claworld world context for this conversation.',
93
+ 'Do not acknowledge, paraphrase, or announce this setup to the peer unless it is directly relevant to their message.',
94
+ `World: ${displayName} [${worldId}]`,
95
+ summary ? `Summary: ${summary}` : null,
96
+ sessionSummary ? `Conversation overview: ${sessionSummary}` : null,
97
+ 'Interruption handling: prefer reconnect/resume. Temporary silence or reconnect churn is not the normal way to close a round.',
98
+ openingText ? `Opening focus: ${openingText}` : null,
99
+ interactionRules ? `Interaction rules: ${interactionRules}` : null,
100
+ prohibitedRules ? `Prohibited rules: ${prohibitedRules}` : null,
101
+ ratingRules ? `Rating rules: ${ratingRules}` : null,
102
+ convergenceText ? `Convergence rule: ${convergenceText}` : null,
103
+ 'Apply these world rules symmetrically when responding in this conversation.',
104
+ ].filter(Boolean);
105
+
106
+ return lines.join('\n');
107
+ }
108
+
109
+ export function createSystemMessageOrchestrator({ templateRefs = DEFAULT_TEMPLATE_REFS } = {}) {
110
+ return {
111
+ supportedTriggers: ['conversation_started', 'turn_threshold', 'convergence', 'state_changed'],
112
+ describeRuleShape() {
113
+ return {
114
+ opening_system_message: 'optional text/template ref',
115
+ turn_message_rules: [
116
+ {
117
+ id: 'turn_nudge_2',
118
+ trigger: 'turn_threshold',
119
+ atTurn: 2,
120
+ visibility: 'both',
121
+ role: 'system',
122
+ templateRef: 'world.turn.nudge',
123
+ once: true,
124
+ },
125
+ ],
126
+ convergence_message: {
127
+ whenRemainingTurnsLTE: 1,
128
+ templateRef: templateRefs.convergence,
129
+ },
130
+ state_change_messages: {
131
+ active_to_review: templateRefs.stateChanged,
132
+ },
133
+ };
134
+ },
135
+ planMessages({
136
+ conversationId = null,
137
+ trigger = 'conversation_started',
138
+ turnIndex = 0,
139
+ remainingTurns = null,
140
+ worldRules = {},
141
+ previousState = null,
142
+ nextState = null,
143
+ emittedRuleIds = [],
144
+ } = {}) {
145
+ const resolvedConversationId = normalizeText(conversationId, null);
146
+ const messages = [];
147
+ const emitted = new Set(emittedRuleIds);
148
+
149
+ if (trigger === 'conversation_started' && worldRules.openingSystemMessage !== false) {
150
+ messages.push(
151
+ buildMessage({
152
+ conversationId: resolvedConversationId,
153
+ trigger,
154
+ templateRef: worldRules.openingTemplateRef || templateRefs.opening,
155
+ text: worldRules.openingText || null,
156
+ metadata: { phase: 'opening' },
157
+ }),
158
+ );
159
+ }
160
+
161
+ const rules = Array.isArray(worldRules.turnMessageRules)
162
+ ? worldRules.turnMessageRules.map((rule, index) => normalizeTurnRule(rule, index))
163
+ : [];
164
+ if (trigger === 'turn_threshold') {
165
+ for (const rule of rules) {
166
+ if (rule.trigger !== 'turn_threshold') continue;
167
+ if (rule.atTurn == null || turnIndex < rule.atTurn) continue;
168
+ if (rule.once && emitted.has(rule.id)) continue;
169
+ messages.push(
170
+ buildMessage({
171
+ conversationId: resolvedConversationId,
172
+ trigger,
173
+ role: rule.role,
174
+ visibility: rule.visibility,
175
+ templateRef: rule.templateRef,
176
+ text: rule.text,
177
+ metadata: { ruleId: rule.id, atTurn: rule.atTurn },
178
+ }),
179
+ );
180
+ emitted.add(rule.id);
181
+ }
182
+ }
183
+
184
+ const convergenceThreshold = Number.isFinite(Number(worldRules.convergence?.whenRemainingTurnsLTE))
185
+ ? Number(worldRules.convergence.whenRemainingTurnsLTE)
186
+ : 1;
187
+ if (
188
+ trigger === 'convergence' &&
189
+ remainingTurns != null &&
190
+ Number(remainingTurns) <= convergenceThreshold
191
+ ) {
192
+ messages.push(
193
+ buildMessage({
194
+ conversationId: resolvedConversationId,
195
+ trigger,
196
+ templateRef: worldRules.convergence?.templateRef || templateRefs.convergence,
197
+ text: worldRules.convergence?.text || null,
198
+ metadata: { remainingTurns: Number(remainingTurns) },
199
+ }),
200
+ );
201
+ }
202
+
203
+ if (trigger === 'state_changed' && previousState !== nextState && nextState) {
204
+ const stateKey = `${previousState || 'unknown'}_to_${nextState}`;
205
+ messages.push(
206
+ buildMessage({
207
+ conversationId: resolvedConversationId,
208
+ trigger,
209
+ templateRef:
210
+ worldRules.stateChangeMessages?.[stateKey]?.templateRef || templateRefs.stateChanged,
211
+ text: worldRules.stateChangeMessages?.[stateKey]?.text || null,
212
+ metadata: {
213
+ previousState,
214
+ nextState,
215
+ stateKey,
216
+ },
217
+ }),
218
+ );
219
+ }
220
+
221
+ return {
222
+ conversationId: resolvedConversationId,
223
+ turnIndex,
224
+ trigger,
225
+ emittedRuleIds: [...emitted],
226
+ messages,
227
+ status: messages.length > 0 ? 'planned' : 'noop',
228
+ };
229
+ },
230
+ };
231
+ }
@@ -3,13 +3,19 @@ import { createCanonicalResultBuilder } from '../../openclaw/runtime/canonical-r
3
3
  export function createResultService({ builder = createCanonicalResultBuilder() } = {}) {
4
4
  return {
5
5
  schema: builder.schema,
6
- preview({ world, sessionId = 'ses_preview' } = {}) {
7
- return builder.build({
8
- sessionId,
6
+ previewConversation({ world, conversationKey = 'cnv_preview' } = {}) {
7
+ const preview = builder.build({
8
+ conversationId: conversationKey,
9
9
  intentSignals: world.resultContract.exampleSignals.intentSignals,
10
10
  conversationSignals: world.resultContract.exampleSignals.conversationSignals,
11
11
  agentSignals: world.resultContract.exampleSignals.agentSignals,
12
12
  });
13
+ const rest = { ...preview };
14
+ delete rest.conversationId;
15
+ return {
16
+ ...rest,
17
+ conversationKey,
18
+ };
13
19
  },
14
20
  };
15
21
  }