@coolclaw/coolclaw 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,13 +20,13 @@
20
20
  - `PRIVATE_MESSAGE`
21
21
  - `GROUP_MESSAGE`
22
22
  - `SYSTEM_NOTIFICATION`
23
- - `GAME_EVENT` — werewolf-game phase events (`WOLF_TURN`/`WITCH_TURN`/`SEER_TURN`/`DAY_SPEAK_TURN`/`LAST_WORD_TURN`/`DAY_VOTE_TURN`/`HUNTER_SKILL_TURN`); LLM reply must contain a `<ACTION>{...}</ACTION>` block which is POSTed to `/riddle/api/chat/agent/action`. See `docs/game-event-integration.md` for details.
23
+ - `GAME_EVENT` — backend-owned `agentTask` events. The plugin uses `agentTask.renderedPrompt` verbatim, validates the parsed `<ACTION>{...}</ACTION>` only against `agentTask.actionContract`, and submits a WS `GAME_ACTION` frame with prompt/action audit fields. See `docs/game-event-integration.md` for details.
24
24
  - `CONTENT_TASK`
25
25
  - `AGENT_NOTIFY` — Riddle content module 主动通知帧(`POST_COMMENTED` / `COMMENT_REPLIED` / `POST_RECOMMEND`),`shouldReply: false`,仅用于驱动 Agent 感知新帖 / 被评论 / 被回复事件。
26
26
 
27
27
  ## Requirements
28
28
 
29
- - Node.js **>= 18** (required for global `fetch` + `AbortSignal.timeout` used by the GAME_EVENT action client).
29
+ - Node.js **>= 18**.
30
30
 
31
31
  ## Installation
32
32
 
@@ -99,7 +99,7 @@ The channel account config written by setup uses `tokenSecretRef: file://...` by
99
99
  - `allowlist`: block unknown private-message senders.
100
100
  - `pairing`: route unknown private-message senders to a pairing conversation and do not trigger model replies.
101
101
 
102
- Group messages trigger model replies only when the Riddle frame has `mentioned=true`. MVP notification frames are dispatched as notifications and do not trigger model replies by default.
102
+ Group messages trigger model replies only when the Riddle frame has `mentioned=true`. GAME_EVENT frames trigger model replies only when the backend includes a dispatchable `agentTask`.
103
103
 
104
104
  ## OpenClaw Compatibility
105
105
 
@@ -3,7 +3,7 @@ import {
3
3
  coolclawChannelPlugin,
4
4
  defaultBindingFile,
5
5
  setCoolclawRuntime
6
- } from "./chunk-QNBJDZKJ.js";
6
+ } from "./chunk-QKB2R55C.js";
7
7
 
8
8
  // index.ts
9
9
  import { defineChannelPluginEntry, buildChannelConfigSchema } from "openclaw/plugin-sdk/core";
@@ -120,6 +120,134 @@ var FileAckStore = class {
120
120
  }
121
121
  };
122
122
 
123
+ // src/agent-task.ts
124
+ import { createHash } from "crypto";
125
+ function normalizeAgentTask(value) {
126
+ if (!isRecord(value)) return null;
127
+ const actionContract = normalizeActionContract(value.actionContract);
128
+ const fallbackAction = isRecord(value.fallbackAction) ? value.fallbackAction : null;
129
+ return {
130
+ agentTaskVersion: readNumber(value.agentTaskVersion),
131
+ promptPolicyVersion: readString(value.promptPolicyVersion),
132
+ requiresReply: value.requiresReply === true,
133
+ renderedPrompt: readString(value.renderedPrompt) ?? "",
134
+ renderedPromptHash: readString(value.renderedPromptHash),
135
+ actionFormat: readString(value.actionFormat),
136
+ conversationKey: readString(value.conversationKey),
137
+ actionContract,
138
+ fallbackAction,
139
+ diagnostics: isRecord(value.diagnostics) ? value.diagnostics : void 0
140
+ };
141
+ }
142
+ function isDispatchableAgentTask(task) {
143
+ return task.requiresReply === true && task.renderedPrompt.trim().length > 0 && allowedActionTypes(task).length > 0;
144
+ }
145
+ function validateAgentAction(action, task) {
146
+ if (!isRecord(action.actionData)) {
147
+ return { ok: false, reason: "invalid_action_shape" };
148
+ }
149
+ const allowed = allowedActionTypes(task);
150
+ if (allowed.length === 0) {
151
+ return { ok: false, reason: "missing_contract" };
152
+ }
153
+ const option = task.actionContract.options.find((candidate) => candidate.actionType === action.actionType);
154
+ if (!option) {
155
+ return { ok: false, reason: "disallowed_action_type" };
156
+ }
157
+ if (!matchesActionDataSchema(action.actionData, option.actionDataSchema)) {
158
+ return { ok: false, reason: "invalid_action_shape" };
159
+ }
160
+ return { ok: true, action };
161
+ }
162
+ function backendFallbackAction(task) {
163
+ const fallback = task.fallbackAction;
164
+ if (!isRecord(fallback) || typeof fallback.actionType !== "string") return null;
165
+ if (!isRecord(fallback.actionData)) return null;
166
+ const parsed = {
167
+ actionType: fallback.actionType,
168
+ actionData: fallback.actionData
169
+ };
170
+ const validated = validateAgentAction(parsed, task);
171
+ return validated.ok ? validated.action : null;
172
+ }
173
+ function sha256Hex(text) {
174
+ return createHash("sha256").update(text).digest("hex");
175
+ }
176
+ function rawResponsePreview(text, maxChars = 5e3) {
177
+ if (text.length === 0) return void 0;
178
+ return text.length <= maxChars ? text : `${text.slice(0, maxChars)}...`;
179
+ }
180
+ function normalizeActionContract(value) {
181
+ if (!isRecord(value)) return { options: [] };
182
+ const options = Array.isArray(value.options) ? value.options.flatMap((option) => normalizeActionOption(option)) : [];
183
+ return {
184
+ options,
185
+ finalOutputRules: Array.isArray(value.finalOutputRules) ? value.finalOutputRules.filter((rule) => typeof rule === "string") : void 0
186
+ };
187
+ }
188
+ function normalizeActionOption(value) {
189
+ if (!isRecord(value) || typeof value.actionType !== "string" || value.actionType.length === 0) {
190
+ return [];
191
+ }
192
+ return [{
193
+ actionType: value.actionType,
194
+ actionDataSchema: isRecord(value.actionDataSchema) ? value.actionDataSchema : void 0,
195
+ description: readString(value.description)
196
+ }];
197
+ }
198
+ function allowedActionTypes(task) {
199
+ return task.actionContract.options.map((option) => option.actionType);
200
+ }
201
+ function matchesActionDataSchema(actionData, schema) {
202
+ if (!schema || Object.keys(schema).length === 0) return true;
203
+ if (typeof schema.type === "string" && schema.type !== "object") return false;
204
+ const properties = isRecord(schema.properties) ? schema.properties : {};
205
+ const required = Array.isArray(schema.required) ? schema.required.filter((field) => typeof field === "string" && field.length > 0) : [];
206
+ for (const field of required) {
207
+ if (!Object.prototype.hasOwnProperty.call(actionData, field)) return false;
208
+ if (!matchesSchemaValue(actionData[field], properties[field], true)) return false;
209
+ }
210
+ for (const [field, value] of Object.entries(actionData)) {
211
+ const propertySchema = properties[field];
212
+ if (propertySchema !== void 0 && !matchesSchemaValue(value, propertySchema, false)) {
213
+ return false;
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+ function matchesSchemaValue(value, schema, required) {
219
+ if (value == null) return !required;
220
+ if (!isRecord(schema)) return true;
221
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
222
+ return false;
223
+ }
224
+ switch (schema.type) {
225
+ case "integer":
226
+ return typeof value === "number" && Number.isInteger(value);
227
+ case "number":
228
+ return typeof value === "number" && Number.isFinite(value);
229
+ case "string":
230
+ return typeof value === "string" && (typeof schema.minLength !== "number" || value.length >= schema.minLength) && (typeof schema.maxLength !== "number" || value.length <= schema.maxLength);
231
+ case "boolean":
232
+ return typeof value === "boolean";
233
+ case "object":
234
+ return isRecord(value);
235
+ case "array":
236
+ return Array.isArray(value);
237
+ default:
238
+ return true;
239
+ }
240
+ }
241
+ function readString(value) {
242
+ return typeof value === "string" && value.length > 0 ? value : void 0;
243
+ }
244
+ function readNumber(value) {
245
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
246
+ }
247
+ function isRecord(value) {
248
+ return typeof value === "object" && value !== null && !Array.isArray(value);
249
+ }
250
+
123
251
  // src/frame-codec.ts
124
252
  import { randomUUID } from "crypto";
125
253
  var CoolclawFrameDecodeError = class extends Error {
@@ -147,7 +275,7 @@ function decodeFrame(raw) {
147
275
  } catch (error) {
148
276
  throw new CoolclawFrameDecodeError(`Invalid CoolClaw frame JSON: ${error.message}`);
149
277
  }
150
- if (!isRecord(value)) {
278
+ if (!isRecord2(value)) {
151
279
  throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: expected object");
152
280
  }
153
281
  if (!("v" in value)) {
@@ -182,304 +310,10 @@ function decodeFrame(raw) {
182
310
  }
183
311
  return frame;
184
312
  }
185
- function isRecord(value) {
313
+ function isRecord2(value) {
186
314
  return typeof value === "object" && value !== null;
187
315
  }
188
316
 
189
- // src/game-event-prompt.ts
190
- function asRecord(v) {
191
- return typeof v === "object" && v !== null ? v : {};
192
- }
193
- function asString(v, fallback = "") {
194
- return typeof v === "string" ? v : fallback;
195
- }
196
- function asNumberOrNull(v) {
197
- return typeof v === "number" && Number.isFinite(v) ? v : null;
198
- }
199
- function asNumberArray(v) {
200
- return Array.isArray(v) ? v.filter((x) => typeof x === "number") : [];
201
- }
202
- function asRecordArray(v) {
203
- return Array.isArray(v) ? v.map(asRecord) : [];
204
- }
205
- function asStringArray(v) {
206
- return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
207
- }
208
- function renderPlayerInfo(list, selfSeat) {
209
- if (list.length === 0) return "\uFF08\u65E0\u5EA7\u4F4D\u4FE1\u606F\uFF09";
210
- return list.map((p) => {
211
- const seat = asNumberOrNull(p.seat);
212
- const playerStatus = p.alive === true ? "\u5B58\u6D3B" : p.alive === false ? "\u5DF2\u6B7B\u4EA1" : "\u53C2\u8D5B";
213
- if (seat != null && selfSeat != null && seat === selfSeat) {
214
- const name = asString(p.name, "\u672A\u77E5");
215
- const voice = asString(p.voiceDesc, "");
216
- return `\u5EA7\u4F4D${seat}\uFF08\u4F60\u81EA\u5DF1\uFF0C${name}${voice ? `\uFF0C${voice}` : ""}\uFF0C${playerStatus}\uFF09`;
217
- }
218
- return `\u5EA7\u4F4D${seat ?? "?"}\uFF08${playerStatus}\uFF09`;
219
- }).join("\uFF1B");
220
- }
221
- function stripAudioSuffix(line) {
222
- return line.replace(/\s*:audio=https?:\/\/\S+/g, "").replace(/\s*:cb=[^\s|]+/g, "");
223
- }
224
- function renderHistory(history) {
225
- if (history.length === 0) return "\uFF08\u6682\u65E0\u5386\u53F2\u8BB0\u5F55\uFF09";
226
- return history.map((h, i) => `${i + 1}. ${stripAudioSuffix(h)}`).join("\n");
227
- }
228
- function renderAliveSeats(seats) {
229
- return seats.length === 0 ? "\uFF08\u65E0\uFF09" : `[${seats.join(", ")}]`;
230
- }
231
- function asMvpCandidates(v) {
232
- if (!Array.isArray(v)) return [];
233
- return v.flatMap((item) => {
234
- if (typeof item === "number" && Number.isFinite(item)) {
235
- return [{ agentId: item, seatNumber: null }];
236
- }
237
- const record = asRecord(item);
238
- const agentId = asNumberOrNull(record.agentId) ?? asNumberOrNull(record.targetAgentId);
239
- if (agentId == null) return [];
240
- const seatNumber = asNumberOrNull(record.seatNumber) ?? asNumberOrNull(record.seat);
241
- return [{ agentId, seatNumber }];
242
- });
243
- }
244
- function renderMvpCandidateList(candidates, selfAgentId) {
245
- if (candidates.length === 0) return "\uFF08\u5019\u9009\u4FE1\u606F\u7F3A\u5931\uFF1B\u7B49\u5F85\u5E73\u53F0\u8D85\u65F6\u515C\u5E95\uFF09";
246
- return candidates.map((candidate) => {
247
- const seat = candidate.seatNumber == null ? "?" : candidate.seatNumber;
248
- const selfMark = selfAgentId != null && candidate.agentId === selfAgentId ? "\uFF08\u4F60\u81EA\u5DF1\uFF0C\u4E0D\u80FD\u6295\u7ED9\u81EA\u5DF1\uFF09" : "";
249
- return `- \u5EA7\u4F4D${seat} / Agent ${candidate.agentId}${selfMark}`;
250
- }).join("\n");
251
- }
252
- function renderHeader(eventType, outer, payload) {
253
- const round = asNumberOrNull(outer.round) ?? 1;
254
- const selfSeat = asNumberOrNull(payload.selfSeat);
255
- const selfRole = asString(payload.selfRole, "\u672A\u77E5");
256
- const selfName = asString(payload.selfAgentName, "");
257
- const phase = eventType === "MVP_VOTE_REQUEST" ? "\u8D5B\u540E" : eventType.startsWith("DAY_") || eventType === "LAST_WORD_TURN" || eventType === "HUNTER_SKILL_TURN" ? "\u767D\u5929" : "\u591C\u665A";
258
- const aliveSeats = asNumberArray(payload.aliveSeats);
259
- const seatsLabel = eventType === "MVP_VOTE_REQUEST" ? "MVP \u5019\u9009\u5EA7\u4F4D" : "\u5F53\u524D\u5B58\u6D3B\u5EA7\u4F4D";
260
- const playerInfo = renderPlayerInfo(asRecordArray(payload.playerInfoList), selfSeat);
261
- const history = renderHistory(asStringArray(payload.scopedHistory));
262
- return [
263
- `[\u6E38\u620F] \u72FC\u4EBA\u6740 \xB7 \u7B2C ${round} \u8F6E \xB7 ${phase} \xB7 ${describeEventType(eventType)}`,
264
- ``,
265
- `\u4F60\u662F\u5EA7\u4F4D ${selfSeat ?? "?"} \u7684 ${selfRole}${selfName ? `\uFF08${selfName}\uFF09` : ""}\u3002`,
266
- `${seatsLabel}\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
267
- `\u5168\u4F53\u73A9\u5BB6\uFF1A${playerInfo}`,
268
- ``,
269
- `\u3010\u8FD1\u671F\u5386\u53F2\uFF08\u4EC5\u4F60\u53EF\u89C1\uFF09\u3011`,
270
- history,
271
- ``
272
- ].join("\n");
273
- }
274
- function describeEventType(eventType) {
275
- switch (eventType) {
276
- case "WOLF_TURN":
277
- return "\u72FC\u4EBA\u884C\u52A8";
278
- case "WITCH_TURN":
279
- return "\u5973\u5DEB\u884C\u52A8";
280
- case "SEER_TURN":
281
- return "\u9884\u8A00\u5BB6\u67E5\u9A8C";
282
- case "DAY_SPEAK_TURN":
283
- return "\u767D\u5929\u53D1\u8A00";
284
- case "LAST_WORD_TURN":
285
- return "\u9057\u8A00";
286
- case "DAY_VOTE_TURN":
287
- return "\u767D\u5929\u6295\u7968";
288
- case "HUNTER_SKILL_TURN":
289
- return "\u730E\u4EBA\u6280\u80FD";
290
- case "MVP_VOTE_REQUEST":
291
- return "MVP \u6295\u7968";
292
- default:
293
- return eventType;
294
- }
295
- }
296
- function renderTask(eventType, payload) {
297
- switch (eventType) {
298
- case "WOLF_TURN":
299
- return renderWolfTurn(payload);
300
- case "WITCH_TURN":
301
- return renderWitchTurn(payload);
302
- case "SEER_TURN":
303
- return renderSeerTurn(payload);
304
- case "DAY_SPEAK_TURN":
305
- return renderDaySpeakTurn(payload);
306
- case "LAST_WORD_TURN":
307
- return renderLastWordTurn(payload);
308
- case "DAY_VOTE_TURN":
309
- return renderDayVoteTurn(payload);
310
- case "HUNTER_SKILL_TURN":
311
- return renderHunterTurn(payload);
312
- case "MVP_VOTE_REQUEST":
313
- return renderMvpVoteRequest(payload);
314
- default:
315
- return renderUnknownTurn(eventType);
316
- }
317
- }
318
- function renderWolfTurn(payload) {
319
- const aliveSeats = asNumberArray(payload.aliveSeats);
320
- const round = asNumberOrNull(payload.wolfAttemptRound) ?? 1;
321
- const isFirst = payload.isFirstSpeakerInRound === true;
322
- const teammateSeat = asNumberOrNull(payload.teammateSeat);
323
- const tp = asRecord(payload.teammateProposal);
324
- const tpSeat = asNumberOrNull(tp.seat);
325
- const tpTarget = asNumberOrNull(tp.targetSeat);
326
- const tpSpeech = asString(tp.speech, "").trim();
327
- let teammateBlock;
328
- if (tpTarget != null) {
329
- const speechLine = tpSpeech ? `
330
- ===== \u72FC\u961F\u53CB\u53D1\u8A00\u5F00\u59CB =====
331
- ${tpSpeech}
332
- ===== \u72FC\u961F\u53CB\u53D1\u8A00\u7ED3\u675F =====` : "";
333
- teammateBlock = `\u540C\u4F34\uFF08\u5EA7\u4F4D ${tpSeat ?? "?"}\uFF09\u672C\u8F6E\u5DF2\u63D0\u8BAE\u51FB\u6740\u5EA7\u4F4D ${tpTarget}\u3002${speechLine}`;
334
- } else {
335
- teammateBlock = isFirst ? "\u4F60\u662F\u672C\u8F6E\u9996\u4F4D\u53D1\u8A00\u7684\u72FC\u4EBA\u3002" : "\u540C\u4F34\u5C1A\u672A\u53D1\u8A00\u3002";
336
- }
337
- const lastRound = asRecordArray(payload.lastRoundChoices);
338
- const lastRoundStr = lastRound.length > 0 ? `\u4E0A\u4E00\u8F6E\u6295\u7968\u8BB0\u5F55\uFF1A${lastRound.map((c) => `\u5EA7\u4F4D${c.seat}\u2192\u5EA7\u4F4D${c.targetSeat}`).join("\uFF1B")}\u3002` : "";
339
- return [
340
- `\u3010\u4EFB\u52A1\u3011\u72FC\u4EBA\u6740\u4EBA\u534F\u5546\uFF08\u7B2C ${round} \u8F6E\uFF09`,
341
- teammateSeat != null ? `\u4F60\u7684\u72FC\u540C\u4F34\uFF1A\u5EA7\u4F4D ${teammateSeat}\u3002` : "\u4F60\u662F\u72EC\u72FC\u3002",
342
- teammateBlock,
343
- lastRoundStr,
344
- `\u5408\u6CD5\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)} \u4E2D\u7684\u4EFB\u610F\u4E00\u4E2A\u3002`,
345
- ``,
346
- `\u8BF7\u5148\u7B80\u77ED\u9648\u8FF0\u4F60\u7684\u60F3\u6CD5\uFF0C\u7136\u540E\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
347
- `<ACTION>`,
348
- `{"actionType":"WOLF_KILL","actionData":{"targetSeat":<number>,"reason":"<\u7B80\u8FF0>","speech":"<\u53D1\u8A00>"}}`,
349
- `</ACTION>`
350
- ].join("\n");
351
- }
352
- function renderWitchTurn(payload) {
353
- const aliveSeats = asNumberArray(payload.aliveSeats);
354
- const wolfTarget = asNumberOrNull(payload.wolfTargetSeat);
355
- const antidote = payload.antidoteAvailable === true;
356
- const poison = payload.poisonAvailable === true;
357
- const canSelfSave = payload.canSelfSave === true;
358
- const options = [];
359
- if (antidote && wolfTarget != null) {
360
- options.push(
361
- `\u9009\u9879 A\uFF08\u4F7F\u7528\u89E3\u836F\u6551 ${wolfTarget} \u53F7\uFF09\uFF1A`,
362
- `<ACTION>{"actionType":"WITCH_SAVE","actionData":{"speech":"<\u53EF\u9009\u53D1\u8A00>"}}</ACTION>`
363
- );
364
- }
365
- if (poison) {
366
- options.push(
367
- `\u9009\u9879 B\uFF08\u4F7F\u7528\u6BD2\u836F\u6BD2\u6740\u67D0\u5EA7\u4F4D\uFF09\uFF1A`,
368
- `<ACTION>{"actionType":"WITCH_POISON","actionData":{"targetSeat":<number>,"speech":"<\u53EF\u9009\u53D1\u8A00>"}}</ACTION>`
369
- );
370
- }
371
- options.push(
372
- `\u9009\u9879 C\uFF08\u672C\u8F6E\u4E0D\u7528\u836F\uFF09\uFF1A`,
373
- `<ACTION>{"actionType":"WITCH_PASS","actionData":{}}</ACTION>`
374
- );
375
- return [
376
- `\u3010\u4EFB\u52A1\u3011\u5973\u5DEB\u884C\u52A8`,
377
- wolfTarget != null ? `\u4ECA\u665A\u72FC\u4EBA\u76EE\u6807\uFF1A\u5EA7\u4F4D ${wolfTarget}\u3002` : `\u4ECA\u665A\u72FC\u4EBA\u672A\u9009\u62E9\u76EE\u6807\uFF08\u6216\u4F60\u770B\u4E0D\u5230\uFF09\u3002`,
378
- `\u89E3\u836F${antidote ? "\u53EF\u7528" : "\u5DF2\u7528\u8FC7"}\uFF1B\u6BD2\u836F${poison ? "\u53EF\u7528" : "\u5DF2\u7528\u8FC7"}\u3002${antidote && wolfTarget != null ? canSelfSave ? "\uFF08\u672C\u591C\u5141\u8BB8\u81EA\u6551\uFF09" : "\uFF08\u7B2C 2 \u591C\u8D77\u4E0D\u53EF\u81EA\u6551\uFF09" : ""}`,
379
- `\u5408\u6CD5\u6BD2\u836F\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
380
- ``,
381
- `\u8BF7\u4E09\u9009\u4E00\uFF0C\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u5BF9\u5E94\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
382
- ...options
383
- ].join("\n");
384
- }
385
- function renderSeerTurn(payload) {
386
- const aliveSeats = asNumberArray(payload.aliveSeats);
387
- const history = asStringArray(payload.history);
388
- return [
389
- `\u3010\u4EFB\u52A1\u3011\u9884\u8A00\u5BB6\u67E5\u9A8C`,
390
- history.length > 0 ? `\u4F60\u7684\u5386\u53F2\u67E5\u9A8C\u7ED3\u679C\uFF1A${history.join("\uFF1B")}\u3002` : `\u9996\u6B21\u67E5\u9A8C\uFF0C\u5C1A\u65E0\u5386\u53F2\u3002`,
391
- `\u5408\u6CD5\u67E5\u9A8C\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
392
- ``,
393
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
394
- `<ACTION>`,
395
- `{"actionType":"SEER_CHECK","actionData":{"targetSeat":<number>,"speech":"<\u53EF\u9009\u53D1\u8A00>"}}`,
396
- `</ACTION>`
397
- ].join("\n");
398
- }
399
- function renderDaySpeakTurn(payload) {
400
- const maxLen = asNumberOrNull(payload.maxLength) ?? 300;
401
- return [
402
- `\u3010\u4EFB\u52A1\u3011\u767D\u5929\u8F6E\u5230\u4F60\u53D1\u8A00`,
403
- `\u4F60\u9700\u8981\u57FA\u4E8E\u5386\u53F2\u7ED9\u51FA\u6709\u903B\u8F91\u7684\u53D1\u8A00\uFF08\u63A8\u7406\u3001\u7AD9\u8FB9\u3001\u8981\u7968\u7B49\uFF09\uFF0C\u957F\u5EA6\u4E0D\u8D85\u8FC7 ${maxLen} \u5B57\u3002`,
404
- ``,
405
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
406
- `<ACTION>`,
407
- `{"actionType":"DAY_SPEAK","actionData":{"content":"<\u4F60\u7684\u5B8C\u6574\u53D1\u8A00\u6587\u672C>"}}`,
408
- `</ACTION>`
409
- ].join("\n");
410
- }
411
- function renderLastWordTurn(_payload) {
412
- return [
413
- `\u3010\u4EFB\u52A1\u3011\u9057\u8A00`,
414
- `\u4F60\u5DF2\u51FA\u5C40\uFF0C\u53EF\u4EE5\u7559\u4E0B\u9057\u8A00\uFF08\u4E5F\u53EF\u4EE5\u9009\u62E9\u4E0D\u8BF4\uFF09\u3002`,
415
- ``,
416
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
417
- `<ACTION>`,
418
- `{"actionType":"LAST_WORD","actionData":{"content":"<\u9057\u8A00\u6587\u672C\uFF0C\u53EF\u4E3A\u7A7A>"}}`,
419
- `</ACTION>`
420
- ].join("\n");
421
- }
422
- function renderDayVoteTurn(payload) {
423
- const aliveSeats = asNumberArray(payload.aliveSeats);
424
- return [
425
- `\u3010\u4EFB\u52A1\u3011\u767D\u5929\u6295\u7968\u653E\u9010`,
426
- `\u5408\u6CD5\u6295\u7968\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\uFF1B\u53EF\u4EE5\u5F03\u7968\uFF08targetSeat \u586B null\uFF09\u3002`,
427
- ``,
428
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
429
- `<ACTION>`,
430
- `{"actionType":"DAY_VOTE","actionData":{"targetSeat":<number \u6216 null>}}`,
431
- `</ACTION>`
432
- ].join("\n");
433
- }
434
- function renderHunterTurn(payload) {
435
- const aliveSeats = asNumberArray(payload.aliveSeats);
436
- return [
437
- `\u3010\u4EFB\u52A1\u3011\u730E\u4EBA\u6280\u80FD`,
438
- `\u4F60\u88AB\u730E\u4EBA\u6280\u80FD\u89E6\u53D1\uFF08\u6B7B\u4EA1\u65F6\u53EF\u5E26\u8D70\u4E00\u540D\u5176\u4ED6\u73A9\u5BB6\uFF0C\u4E5F\u53EF\u9009\u62E9\u4E0D\u5F00\u67AA\uFF09\u3002`,
439
- `\u5408\u6CD5\u5F00\u67AA\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
440
- ``,
441
- `\u8BF7\u4E8C\u9009\u4E00\uFF1A`,
442
- `\u9009\u9879 A\uFF08\u5F00\u67AA\u5E26\u8D70\u67D0\u5EA7\u4F4D\uFF09\uFF1A`,
443
- `<ACTION>{"actionType":"HUNTER_SHOOT","actionData":{"targetSeat":<number>}}</ACTION>`,
444
- `\u9009\u9879 B\uFF08\u4E0D\u5F00\u67AA\uFF09\uFF1A`,
445
- `<ACTION>{"actionType":"HUNTER_PASS","actionData":{}}</ACTION>`
446
- ].join("\n");
447
- }
448
- function renderMvpVoteRequest(payload) {
449
- const candidates = asMvpCandidates(payload.candidates);
450
- const selfAgentId = asNumberOrNull(payload.selfAgentId);
451
- const round = asNumberOrNull(payload.round) ?? 1;
452
- const deadlineSeconds = asNumberOrNull(payload.deadlineSeconds);
453
- return [
454
- `\u3010\u4EFB\u52A1\u3011\u8D5B\u540E MVP \u6295\u7968\uFF08\u7B2C ${round} \u8F6E\uFF09`,
455
- deadlineSeconds != null ? `\u8BF7\u5728 ${deadlineSeconds} \u79D2\u5185\u4ECE\u5019\u9009\u5BF9\u8C61\u91CC\u9009\u51FA\u4F60\u8BA4\u4E3A\u672C\u5C40\u8D21\u732E\u6700\u5927\u7684 Agent\u3002` : `\u8BF7\u4ECE\u5019\u9009\u5BF9\u8C61\u91CC\u9009\u51FA\u4F60\u8BA4\u4E3A\u672C\u5C40\u8D21\u732E\u6700\u5927\u7684 Agent\u3002`,
456
- `\u5019\u9009\u5BF9\u8C61\uFF08targetAgentId \u5FC5\u987B\u4ECE\u8FD9\u91CC\u9009\uFF0C\u4E0D\u80FD\u6295\u7ED9\u81EA\u5DF1\uFF09\uFF1A`,
457
- renderMvpCandidateList(candidates, selfAgentId),
458
- ``,
459
- `\u8BF7\u7EFC\u5408\u53D1\u8A00\u3001\u6295\u7968\u3001\u5173\u952E\u884C\u52A8\u548C\u80DC\u8D1F\u8D21\u732E\uFF0C\u7B80\u77ED\u8BF4\u660E\u7406\u7531\uFF0C\u7136\u540E\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
460
- `<ACTION>`,
461
- `{"actionType":"MVP_VOTE","actionData":{"targetAgentId":<number>,"round":${round},"reason":"<\u7B80\u8FF0>"}}`,
462
- `</ACTION>`
463
- ].join("\n");
464
- }
465
- function renderUnknownTurn(eventType) {
466
- return [
467
- `\u3010\u63D0\u793A\u3011\u672A\u8BC6\u522B\u7684\u6E38\u620F\u4E8B\u4EF6\u7C7B\u578B\uFF1A${eventType}`,
468
- `\u8BF7\u5FFD\u7565\u672C\u6761\u4E8B\u4EF6\uFF0C\u65E0\u9700\u58F0\u660E\u52A8\u4F5C\u3002`
469
- ].join("\n");
470
- }
471
- function buildGameEventPrompt(eventType, eventData) {
472
- const outer = asRecord(eventData);
473
- const nestedPayload = asRecord(outer.payload);
474
- const payload = Object.keys(nestedPayload).length > 0 ? nestedPayload : outer;
475
- const header = renderHeader(eventType, outer, payload);
476
- const task = renderTask(eventType, payload);
477
- const footer = `
478
-
479
- \uFF08ACTION \u5757\u4E4B\u5916\u7684\u6587\u672C\u4F1A\u4F5C\u4E3A\u65E5\u5FD7\u8BB0\u5F55\uFF0C\u4E0D\u4F1A\u51FA\u73B0\u5728\u6E38\u620F\u4E2D\u3002\uFF09`;
480
- return `${header}${task}${footer}`;
481
- }
482
-
483
317
  // src/inbound.ts
484
318
  function mapInboundFrame(frame) {
485
319
  if (frame.type === "PRIVATE_MESSAGE") {
@@ -533,12 +367,21 @@ function mapInboundFrame(frame) {
533
367
  }
534
368
  async function handleInboundFrame(input) {
535
369
  const envelope = mapInboundFrame(input.frame);
536
- if (!envelope) return;
537
- await ackProcessedSeq(input, envelope);
370
+ if (!envelope) {
371
+ await ackFrameSeq(input);
372
+ return;
373
+ }
374
+ const ackAfterDispatch = envelope.metadata.gameEvent === true;
375
+ if (!ackAfterDispatch) {
376
+ await ackProcessedSeq(input, envelope);
377
+ }
538
378
  await input.dispatch(envelope);
379
+ if (ackAfterDispatch) {
380
+ await ackProcessedSeq(input, envelope);
381
+ }
539
382
  }
540
383
  function mapNotificationFrame(frame) {
541
- const payload = isRecord2(frame.payload) ? frame.payload : {};
384
+ const payload = isRecord3(frame.payload) ? frame.payload : {};
542
385
  const seq = typeof payload.seq === "number" ? payload.seq : void 0;
543
386
  if (frame.type === "SYSTEM_NOTIFICATION") {
544
387
  const title = typeof payload.title === "string" ? payload.title : "System";
@@ -588,7 +431,8 @@ function mapNotificationFrame(frame) {
588
431
  }
589
432
  function mapGameEventFrame(frame, payload) {
590
433
  const eventType = typeof payload.eventType === "string" ? payload.eventType : "UNKNOWN";
591
- if (eventType === "GAME_START") {
434
+ const agentTask = normalizeAgentTask(payload.agentTask);
435
+ if (!agentTask || !isDispatchableAgentTask(agentTask)) {
592
436
  return null;
593
437
  }
594
438
  let eventDataObj = {};
@@ -596,10 +440,10 @@ function mapGameEventFrame(frame, payload) {
596
440
  if (typeof rawEventData === "string" && rawEventData.length > 0) {
597
441
  try {
598
442
  const parsed = JSON.parse(rawEventData);
599
- if (isRecord2(parsed)) eventDataObj = parsed;
443
+ if (isRecord3(parsed)) eventDataObj = parsed;
600
444
  } catch {
601
445
  }
602
- } else if (isRecord2(rawEventData)) {
446
+ } else if (isRecord3(rawEventData)) {
603
447
  eventDataObj = rawEventData;
604
448
  }
605
449
  const gameId = Number(payload.gameId ?? 0);
@@ -618,16 +462,17 @@ function mapGameEventFrame(frame, payload) {
618
462
  traceId,
619
463
  eventType,
620
464
  eventData: eventDataObj,
465
+ agentTask,
466
+ promptPolicyVersion: agentTask.promptPolicyVersion,
467
+ renderedPromptHash: agentTask.renderedPromptHash,
621
468
  deadlineEpochMs,
622
469
  sourceFrameId: frame.id
623
470
  };
624
471
  return {
625
472
  id: eventId,
626
473
  channel: "coolclaw",
627
- // 每个 (roomId, gameId, eventType) 组合作为独立会话键,避免和私聊会话混淆;
628
- // turnSeq 不参与会话 id,以便同一事件类型的多轮对话能沿用同一上下文。
629
- conversationId: `game:${roomId}:${gameId}:${eventType}`,
630
- text: buildGameEventPrompt(eventType, eventDataObj),
474
+ conversationId: agentTask.conversationKey ?? `game:${roomId}:${gameId}:task:${eventId}`,
475
+ text: agentTask.renderedPrompt,
631
476
  messageType: "GAME_EVENT",
632
477
  seq,
633
478
  shouldReply: true,
@@ -636,52 +481,64 @@ function mapGameEventFrame(frame, payload) {
636
481
  }
637
482
  async function ackProcessedSeq(input, envelope) {
638
483
  if (typeof envelope.seq === "number") {
639
- const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
640
- await input.sendAck(createFrame("ACK", { lastAckedSeq }));
484
+ await ackSeq(input, envelope.seq);
641
485
  }
642
486
  }
487
+ async function ackFrameSeq(input) {
488
+ const payload = isRecord3(input.frame.payload) ? input.frame.payload : {};
489
+ const seq = typeof payload.seq === "number" ? payload.seq : void 0;
490
+ if (typeof seq === "number") {
491
+ await ackSeq(input, seq);
492
+ }
493
+ }
494
+ async function ackSeq(input, seq) {
495
+ const current = await input.ackStore.getLastAckedSeq(input.accountKey);
496
+ const lastAckedSeq = seq <= current ? current : seq;
497
+ await input.sendAck(createFrame("ACK", { lastAckedSeq }));
498
+ await input.ackStore.record(input.accountKey, seq);
499
+ }
643
500
  function assertPrivatePayload(value) {
644
- if (!isRecord2(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
501
+ if (!isRecord3(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
645
502
  throw new Error("Invalid PRIVATE_MESSAGE payload");
646
503
  }
647
504
  return {
648
- seq: readNumber(value, "seq"),
649
- messageId: readString(value, "messageId"),
650
- conversationId: readString(value, "conversationId"),
505
+ seq: readNumber2(value, "seq"),
506
+ messageId: readString2(value, "messageId"),
507
+ conversationId: readString2(value, "conversationId"),
651
508
  sender: value.sender,
652
509
  recipient: value.recipient,
653
- messageType: readString(value, "messageType"),
654
- content: readString(value, "content"),
510
+ messageType: readString2(value, "messageType"),
511
+ content: readString2(value, "content"),
655
512
  mentioned: readBoolean(value, "mentioned"),
656
- sentAt: readString(value, "sentAt")
513
+ sentAt: readString2(value, "sentAt")
657
514
  };
658
515
  }
659
516
  function assertGroupPayload(value) {
660
- if (!isRecord2(value) || !isUserRef(value.sender)) {
517
+ if (!isRecord3(value) || !isUserRef(value.sender)) {
661
518
  throw new Error("Invalid GROUP_MESSAGE payload");
662
519
  }
663
520
  return {
664
- seq: readNumber(value, "seq"),
665
- messageId: readString(value, "messageId"),
666
- groupId: readString(value, "groupId"),
667
- groupName: readString(value, "groupName"),
668
- conversationId: readString(value, "conversationId"),
521
+ seq: readNumber2(value, "seq"),
522
+ messageId: readString2(value, "messageId"),
523
+ groupId: readString2(value, "groupId"),
524
+ groupName: readString2(value, "groupName"),
525
+ conversationId: readString2(value, "conversationId"),
669
526
  sender: value.sender,
670
- messageType: readString(value, "messageType"),
671
- content: readString(value, "content"),
527
+ messageType: readString2(value, "messageType"),
528
+ content: readString2(value, "content"),
672
529
  mentioned: readBoolean(value, "mentioned"),
673
- sentAt: readString(value, "sentAt"),
530
+ sentAt: readString2(value, "sentAt"),
674
531
  agentHint: readOptionalString(value, "agentHint")
675
532
  };
676
533
  }
677
- function readString(source, key) {
534
+ function readString2(source, key) {
678
535
  const value = source[key];
679
536
  if (typeof value !== "string" || value.length === 0) {
680
537
  throw new Error(`Invalid inbound payload: missing ${key}`);
681
538
  }
682
539
  return value;
683
540
  }
684
- function readNumber(source, key) {
541
+ function readNumber2(source, key) {
685
542
  const value = source[key];
686
543
  if (typeof value !== "number" || !Number.isInteger(value)) {
687
544
  throw new Error(`Invalid inbound payload: missing ${key}`);
@@ -706,9 +563,9 @@ function readOptionalString(source, key) {
706
563
  return value;
707
564
  }
708
565
  function isUserRef(value) {
709
- return isRecord2(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
566
+ return isRecord3(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
710
567
  }
711
- function isRecord2(value) {
568
+ function isRecord3(value) {
712
569
  return typeof value === "object" && value !== null;
713
570
  }
714
571
 
@@ -833,13 +690,23 @@ async function sendMedia(input) {
833
690
  async function sendGameAction(input) {
834
691
  const frame = createFrame("GAME_ACTION", {
835
692
  gameId: input.gameId,
693
+ roomId: input.roomId,
694
+ eventType: input.eventType,
836
695
  actionType: input.actionType,
837
696
  actionData: input.actionData,
838
697
  // AgentActionRequest.timestamp 契约为 String
839
698
  timestamp: String(Date.now()),
840
699
  turnSeq: input.turnSeq,
841
700
  eventId: input.eventId,
842
- traceId: input.traceId
701
+ traceId: input.traceId,
702
+ promptPolicyVersion: input.promptPolicyVersion,
703
+ renderedPromptHash: input.renderedPromptHash,
704
+ parseSource: input.parseSource,
705
+ rawResponseHash: input.rawResponseHash,
706
+ rawResponsePreview: input.rawResponsePreview,
707
+ modelActionRejected: input.modelActionRejected,
708
+ modelActionType: input.modelActionType,
709
+ validationReason: input.validationReason
843
710
  });
844
711
  const response = await input.client.request(frame);
845
712
  if (response.ok === false) {
@@ -849,7 +716,7 @@ async function sendGameAction(input) {
849
716
 
850
717
  // src/game-action-parser.ts
851
718
  function extractActionBlock(text) {
852
- const re = /<ACTION>\s*([\s\S]+?)\s*<\/ACTION>/g;
719
+ const re = /<ACTION>\s*([\s\S]+?)\s*<\/ACTION>/gi;
853
720
  let match;
854
721
  let last = null;
855
722
  while ((match = re.exec(text)) !== null) {
@@ -857,6 +724,11 @@ function extractActionBlock(text) {
857
724
  }
858
725
  return last;
859
726
  }
727
+ function normalizeActionBlock(body) {
728
+ const trimmed = body.trim();
729
+ const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed);
730
+ return fenced ? fenced[1].trim() : trimmed;
731
+ }
860
732
  function parseAgentAction(text) {
861
733
  if (typeof text !== "string" || text.length === 0) {
862
734
  return { error: "no_action_block" };
@@ -867,7 +739,7 @@ function parseAgentAction(text) {
867
739
  }
868
740
  let obj;
869
741
  try {
870
- obj = JSON.parse(body);
742
+ obj = JSON.parse(normalizeActionBlock(body));
871
743
  } catch (e) {
872
744
  return { error: "invalid_json", detail: e instanceof Error ? e.message : String(e) };
873
745
  }
@@ -887,105 +759,44 @@ function parseAgentAction(text) {
887
759
  };
888
760
  }
889
761
 
890
- // src/game-action-fallback.ts
891
- function asNumberArray2(v) {
892
- return Array.isArray(v) ? v.filter((x) => typeof x === "number") : [];
893
- }
894
- function asNumberOrNull2(v) {
895
- return typeof v === "number" && Number.isFinite(v) ? v : null;
896
- }
897
- function asRecord2(v) {
898
- return typeof v === "object" && v !== null ? v : {};
899
- }
900
- function hasKeys(record) {
901
- return Object.keys(record).length > 0;
902
- }
903
- function candidateAgentIds(v) {
904
- if (!Array.isArray(v)) return [];
905
- return v.flatMap((item) => {
906
- if (typeof item === "number" && Number.isFinite(item)) return [item];
907
- const record = asRecord2(item);
908
- const agentId = asNumberOrNull2(record.agentId) ?? asNumberOrNull2(record.targetAgentId);
909
- return agentId == null ? [] : [agentId];
910
- });
762
+ // src/game-action-audit.ts
763
+ function normalizeAuditText(value, maxChars = 256) {
764
+ if (!value) return void 0;
765
+ return value.length <= maxChars ? value : value.slice(0, maxChars);
911
766
  }
912
- function pickRandomAlive(aliveSeats, excludeSeat) {
913
- const candidates = excludeSeat != null ? aliveSeats.filter((s) => s !== excludeSeat) : aliveSeats;
914
- if (candidates.length === 0) return null;
915
- return candidates[Math.floor(Math.random() * candidates.length)];
916
- }
917
- function pickRandomAgent(candidates, excludeAgentId) {
918
- const eligible = excludeAgentId == null ? candidates : candidates.filter((candidate) => candidate !== excludeAgentId);
919
- if (eligible.length === 0) return candidates[0] ?? null;
920
- return eligible[Math.floor(Math.random() * eligible.length)];
921
- }
922
- function fallbackActionFor(eventType, eventData) {
923
- const outer = asRecord2(eventData);
924
- const nestedPayload = asRecord2(outer.payload);
925
- const payload = hasKeys(nestedPayload) ? nestedPayload : outer;
926
- const aliveSeats = asNumberArray2(payload.aliveSeats);
927
- const selfSeat = typeof payload.selfSeat === "number" ? payload.selfSeat : void 0;
928
- switch (eventType) {
929
- case "WOLF_TURN": {
930
- const target = pickRandomAlive(aliveSeats, selfSeat);
931
- return {
932
- actionType: "WOLF_KILL",
933
- actionData: {
934
- targetSeat: target ?? (aliveSeats[0] ?? 1),
935
- reason: "\uFF08\u6258\u7BA1\uFF1ALLM \u672A\u7ED9\u51FA\u5408\u6CD5\u52A8\u4F5C\uFF09",
936
- speech: ""
937
- }
938
- };
939
- }
940
- case "WITCH_TURN": {
941
- return { actionType: "WITCH_PASS", actionData: {} };
942
- }
943
- case "SEER_TURN": {
944
- const target = pickRandomAlive(aliveSeats, selfSeat);
945
- return {
946
- actionType: "SEER_CHECK",
947
- actionData: {
948
- targetSeat: target ?? (aliveSeats[0] ?? 1),
949
- speech: ""
950
- }
951
- };
952
- }
953
- case "DAY_SPEAK_TURN":
954
- return {
955
- actionType: "DAY_SPEAK",
956
- actionData: { content: "\uFF08\u6258\u7BA1\u53D1\u8A00\uFF1A\u672C\u8F6E\u8DF3\u8FC7\uFF09" }
957
- };
958
- case "LAST_WORD_TURN":
959
- return {
960
- actionType: "LAST_WORD",
961
- actionData: { content: "" }
962
- };
963
- case "DAY_VOTE_TURN":
964
- return {
965
- actionType: "DAY_VOTE",
966
- actionData: { targetSeat: null }
967
- };
968
- case "HUNTER_SKILL_TURN":
969
- return { actionType: "HUNTER_PASS", actionData: {} };
970
- case "MVP_VOTE_REQUEST": {
971
- const selfAgentId = asNumberOrNull2(payload.selfAgentId);
972
- const candidates = candidateAgentIds(payload.candidates);
973
- const targetAgentId = pickRandomAgent(candidates, selfAgentId);
974
- if (targetAgentId == null) {
975
- return { actionType: "UNKNOWN", actionData: {} };
976
- }
977
- return {
978
- actionType: "MVP_VOTE",
979
- actionData: {
980
- targetAgentId,
981
- round: asNumberOrNull2(payload.round) ?? asNumberOrNull2(outer.round) ?? 1,
982
- reason: "\uFF08\u6258\u7BA1\uFF1ALLM \u672A\u7ED9\u51FA\u5408\u6CD5 MVP \u6295\u7968\uFF09"
983
- }
984
- };
985
- }
986
- default:
987
- return { actionType: "UNKNOWN", actionData: {} };
767
+ function inferRejectedModelAction(rawResponse, task) {
768
+ if (rawResponse.trim().length === 0) {
769
+ return {
770
+ modelActionRejected: false,
771
+ validationReason: "no_model_output"
772
+ };
773
+ }
774
+ const parsed = parseAgentAction(rawResponse);
775
+ if ("error" in parsed) {
776
+ return {
777
+ modelActionRejected: true,
778
+ validationReason: parseErrorReason(parsed)
779
+ };
988
780
  }
781
+ const validation = validateAgentAction(parsed, task);
782
+ if (!validation.ok) {
783
+ return {
784
+ modelActionRejected: true,
785
+ modelActionType: parsed.actionType,
786
+ validationReason: validation.reason
787
+ };
788
+ }
789
+ return {
790
+ modelActionRejected: true,
791
+ modelActionType: parsed.actionType,
792
+ validationReason: "valid_action_not_submitted"
793
+ };
794
+ }
795
+ function parseErrorReason(error) {
796
+ if (error.error === "invalid_json") {
797
+ return normalizeAuditText(`invalid_json:${error.detail}`) ?? "invalid_json";
798
+ }
799
+ return error.error;
989
800
  }
990
801
 
991
802
  // src/ws-client.ts
@@ -1200,18 +1011,18 @@ var CoolclawWsClient = class {
1200
1011
  }
1201
1012
  };
1202
1013
  function readPingInterval(payload) {
1203
- if (!isRecord3(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
1014
+ if (!isRecord4(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
1204
1015
  return void 0;
1205
1016
  }
1206
1017
  return payload.pingIntervalMs;
1207
1018
  }
1208
1019
  function readErrorMessage(payload) {
1209
- if (isRecord3(payload) && typeof payload.message === "string") {
1020
+ if (isRecord4(payload) && typeof payload.message === "string") {
1210
1021
  return payload.message;
1211
1022
  }
1212
1023
  return "CoolClaw request failed";
1213
1024
  }
1214
- function isRecord3(value) {
1025
+ function isRecord4(value) {
1215
1026
  return typeof value === "object" && value !== null;
1216
1027
  }
1217
1028
 
@@ -1265,35 +1076,75 @@ function logAckFailure(params) {
1265
1076
  const target = params.target ? ` target=${params.target}` : "";
1266
1077
  params.log(`${params.channel} ack cleanup failed${target}: ${String(params.error)}`);
1267
1078
  }
1268
- async function submitGameActionWithLog(action, meta, wsClient, log, source) {
1079
+ async function submitGameActionWithLog(action, meta, wsClient, log, source, rawResponse, auditMeta) {
1269
1080
  if (!wsClient.isConnected()) {
1270
1081
  log?.error?.(`[GAME-ACTION] submit skipped: ws not connected eventId=${meta.eventId}`);
1271
- return;
1082
+ return false;
1272
1083
  }
1084
+ const responseHash = rawResponse && rawResponse.length > 0 ? sha256Hex(rawResponse) : void 0;
1273
1085
  log?.info?.(
1274
- `[GAME-ACTION] submit start source=${source} eventType=${meta.eventType} actionType=${action.actionType} gameId=${meta.gameId} turnSeq=${meta.turnSeq} eventId=${meta.eventId}`
1086
+ `[GAME-ACTION] submit start source=${source} eventType=${meta.eventType} actionType=${action.actionType} gameId=${meta.gameId} roomId=${meta.roomId} turnSeq=${meta.turnSeq} eventId=${meta.eventId} promptPolicyVersion=${meta.promptPolicyVersion ?? ""} renderedPromptHash=${meta.renderedPromptHash ?? ""} rawResponseHash=${responseHash ?? ""}`
1275
1087
  );
1276
1088
  const start = Date.now();
1277
1089
  try {
1278
1090
  await sendGameAction({
1279
1091
  client: wsClient,
1280
1092
  gameId: meta.gameId,
1093
+ roomId: meta.roomId,
1094
+ eventType: meta.eventType,
1281
1095
  actionType: action.actionType,
1282
1096
  actionData: action.actionData,
1283
1097
  turnSeq: meta.turnSeq,
1284
1098
  eventId: meta.eventId,
1285
- traceId: meta.traceId
1099
+ traceId: meta.traceId,
1100
+ promptPolicyVersion: meta.promptPolicyVersion,
1101
+ renderedPromptHash: meta.renderedPromptHash,
1102
+ parseSource: source,
1103
+ rawResponseHash: responseHash,
1104
+ rawResponsePreview: rawResponse ? rawResponsePreview(rawResponse) : void 0,
1105
+ modelActionRejected: auditMeta?.modelActionRejected,
1106
+ modelActionType: auditMeta?.modelActionType,
1107
+ validationReason: normalizeAuditText(auditMeta?.validationReason)
1286
1108
  });
1287
1109
  log?.info?.(
1288
- `[GAME-ACTION] submit ok source=${source} gameId=${meta.gameId} eventId=${meta.eventId} elapsedMs=${Date.now() - start}`
1110
+ `[GAME-ACTION] submit ok source=${source} gameId=${meta.gameId} eventId=${meta.eventId} promptPolicyVersion=${meta.promptPolicyVersion ?? ""} renderedPromptHash=${meta.renderedPromptHash ?? ""} rawResponseHash=${responseHash ?? ""} elapsedMs=${Date.now() - start}`
1289
1111
  );
1112
+ return true;
1290
1113
  } catch (err) {
1291
1114
  const errMsg = err instanceof Error ? err.message : String(err);
1292
1115
  log?.error?.(
1293
1116
  `[GAME-ACTION] submit failed source=${source} eventId=${meta.eventId} elapsedMs=${Date.now() - start} err=${errMsg}`
1294
1117
  );
1118
+ return false;
1295
1119
  }
1296
1120
  }
1121
+ async function submitBackendFallbackWithLog(params) {
1122
+ const fb = backendFallbackAction(params.meta.agentTask);
1123
+ if (!fb) {
1124
+ params.log?.warn?.(
1125
+ `[GAME-ACTION] backend fallback unavailable eventType=${params.meta.eventType} eventId=${params.meta.eventId} reason=${params.reason} promptPolicyVersion=${params.meta.promptPolicyVersion ?? ""} renderedPromptHash=${params.meta.renderedPromptHash ?? ""}`
1126
+ );
1127
+ return false;
1128
+ }
1129
+ const inferred = inferRejectedModelAction(params.rawResponse ?? "", params.meta.agentTask);
1130
+ const auditMeta = {
1131
+ modelActionRejected: params.auditMeta?.modelActionRejected ?? inferred.modelActionRejected,
1132
+ modelActionType: params.auditMeta?.modelActionType ?? inferred.modelActionType,
1133
+ validationReason: params.auditMeta?.validationReason ?? params.reason ?? inferred.validationReason
1134
+ };
1135
+ params.log?.warn?.(
1136
+ `[GAME-ACTION] backend fallback eventType=${params.meta.eventType} eventId=${params.meta.eventId} reason=${auditMeta.validationReason ?? "unknown"} promptPolicyVersion=${params.meta.promptPolicyVersion ?? ""} renderedPromptHash=${params.meta.renderedPromptHash ?? ""} rawResponseHash=${params.rawResponse ? sha256Hex(params.rawResponse) : ""}`
1137
+ );
1138
+ return submitGameActionWithLog(
1139
+ fb,
1140
+ params.meta,
1141
+ params.wsClient,
1142
+ params.log,
1143
+ "backend_fallback",
1144
+ params.rawResponse || void 0,
1145
+ auditMeta
1146
+ );
1147
+ }
1297
1148
  var runtimeClients = /* @__PURE__ */ new Map();
1298
1149
  function setRuntimeClient(accountKey, client) {
1299
1150
  runtimeClients.set(accountKey, client);
@@ -1441,16 +1292,36 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1441
1292
  accountKey,
1442
1293
  ackStore,
1443
1294
  dispatch: async (envelope) => {
1295
+ const isGameEvent = envelope.metadata?.gameEvent === true;
1296
+ const gameMeta = isGameEvent ? envelope.metadata : null;
1297
+ let gameSubmitted = false;
1298
+ let gameFallbackReason = null;
1299
+ let gameModelActionType;
1300
+ let gameValidationReason;
1301
+ let gameModelActionRejected;
1302
+ const gameBuffer = [];
1303
+ if (isGameEvent && gameMeta) {
1304
+ ctx.log?.info?.(
1305
+ `[GAME-TASK] dispatch start gameId=${gameMeta.gameId} roomId=${gameMeta.roomId} eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} conversationId=${envelope.conversationId}`
1306
+ );
1307
+ }
1444
1308
  const runtime = getCoolclawRuntime();
1445
1309
  if (!runtime?.channel) {
1446
1310
  logInboundDrop({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
1447
1311
  }), channel: "coolclaw", reason: "runtime not available; skipping dispatch" });
1312
+ if (isGameEvent && gameMeta) {
1313
+ const submitted = await submitBackendFallbackWithLog({
1314
+ meta: gameMeta,
1315
+ wsClient,
1316
+ log: ctx.log,
1317
+ reason: "runtime_not_available"
1318
+ });
1319
+ if (!submitted) {
1320
+ throw new Error("game fallback submit failed: runtime_not_available");
1321
+ }
1322
+ }
1448
1323
  return;
1449
1324
  }
1450
- const isGameEvent = envelope.metadata?.gameEvent === true;
1451
- const gameMeta = isGameEvent ? envelope.metadata : null;
1452
- let gameSubmitted = false;
1453
- let gameFallbackReason = null;
1454
1325
  try {
1455
1326
  const isGroup = envelope.conversationId.startsWith("group:");
1456
1327
  const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
@@ -1530,7 +1401,6 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1530
1401
  ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
1531
1402
  }
1532
1403
  });
1533
- const gameBuffer = [];
1534
1404
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1535
1405
  ctx: ctxPayload,
1536
1406
  cfg: ctx.cfg,
@@ -1549,14 +1419,36 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1549
1419
  const full = gameBuffer.join("");
1550
1420
  const parsed = parseAgentAction(full);
1551
1421
  if ("error" in parsed) return;
1552
- gameSubmitted = true;
1553
- await submitGameActionWithLog(
1554
- parsed,
1422
+ const validation = validateAgentAction(parsed, gameMeta.agentTask);
1423
+ if (!validation.ok) {
1424
+ gameModelActionRejected = true;
1425
+ gameModelActionType = parsed.actionType;
1426
+ gameValidationReason = validation.reason;
1427
+ gameFallbackReason = validation.reason;
1428
+ ctx.log?.warn?.(
1429
+ `[GAME-ACTION] rejected model action reason=${validation.reason} actionType=${parsed.actionType} eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawResponseHash=${sha256Hex(full)}`
1430
+ );
1431
+ return;
1432
+ }
1433
+ const submitted = await submitGameActionWithLog(
1434
+ validation.action,
1555
1435
  gameMeta,
1556
1436
  wsClient,
1557
1437
  ctx.log,
1558
- "llm"
1438
+ "llm",
1439
+ full,
1440
+ {
1441
+ modelActionRejected: false,
1442
+ modelActionType: validation.action.actionType
1443
+ }
1559
1444
  );
1445
+ if (submitted) {
1446
+ gameSubmitted = true;
1447
+ } else {
1448
+ gameFallbackReason = `llm_action_submit_failed:${validation.action.actionType}`;
1449
+ gameModelActionRejected = false;
1450
+ gameModelActionType = validation.action.actionType;
1451
+ }
1560
1452
  return;
1561
1453
  }
1562
1454
  try {
@@ -1579,7 +1471,9 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1579
1471
  }
1580
1472
  });
1581
1473
  if (isGameEvent && gameMeta && !gameSubmitted) {
1582
- gameFallbackReason = "no_valid_action_in_llm_output";
1474
+ if (!gameFallbackReason) {
1475
+ gameFallbackReason = "no_valid_action_in_llm_output";
1476
+ }
1583
1477
  }
1584
1478
  } catch (err) {
1585
1479
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -1588,23 +1482,42 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1588
1482
  gameFallbackReason = `dispatch_error: ${errMsg}`;
1589
1483
  }
1590
1484
  } finally {
1485
+ if (isGameEvent && gameMeta) {
1486
+ const rawResponse = gameBuffer.join("");
1487
+ ctx.log?.info?.(
1488
+ `[GAME-TASK] model-output eventId=${gameMeta.eventId} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawHash=${rawResponse ? sha256Hex(rawResponse) : ""} rawPreview=${rawResponsePreview(rawResponse) ?? ""}`
1489
+ );
1490
+ }
1591
1491
  if (isGameEvent && gameMeta && !gameSubmitted) {
1592
1492
  try {
1593
- const fb = fallbackActionFor(gameMeta.eventType, gameMeta.eventData);
1594
- ctx.log?.warn?.(
1595
- `[GAME-ACTION] parse fallback eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} reason=${gameFallbackReason ?? "unknown"}`
1596
- );
1597
- await submitGameActionWithLog(
1598
- fb,
1599
- gameMeta,
1493
+ const rawResponse = gameBuffer.join("");
1494
+ const inferred = inferRejectedModelAction(rawResponse, gameMeta.agentTask);
1495
+ gameModelActionRejected = gameModelActionRejected ?? inferred.modelActionRejected;
1496
+ gameModelActionType = gameModelActionType ?? inferred.modelActionType;
1497
+ gameValidationReason = gameValidationReason ?? inferred.validationReason;
1498
+ if (!gameFallbackReason || gameFallbackReason === "no_valid_action_in_llm_output") {
1499
+ gameFallbackReason = gameValidationReason ?? inferred.validationReason ?? gameFallbackReason;
1500
+ }
1501
+ const submitted = await submitBackendFallbackWithLog({
1502
+ meta: gameMeta,
1600
1503
  wsClient,
1601
- ctx.log,
1602
- "fallback"
1603
- );
1504
+ log: ctx.log,
1505
+ reason: gameFallbackReason ?? gameValidationReason ?? inferred.validationReason ?? "unknown",
1506
+ rawResponse: rawResponse || void 0,
1507
+ auditMeta: {
1508
+ modelActionRejected: gameModelActionRejected,
1509
+ modelActionType: gameModelActionType,
1510
+ validationReason: gameFallbackReason ?? gameValidationReason ?? inferred.validationReason
1511
+ }
1512
+ });
1513
+ if (!submitted) {
1514
+ throw new Error(`game fallback submit failed: ${gameFallbackReason ?? "unknown"}`);
1515
+ }
1604
1516
  } catch (fbErr) {
1605
1517
  ctx.log?.error?.(
1606
1518
  `[GAME-ACTION] fallback submit threw eventId=${gameMeta.eventId} err=${fbErr instanceof Error ? fbErr.message : String(fbErr)}`
1607
1519
  );
1520
+ throw fbErr;
1608
1521
  }
1609
1522
  }
1610
1523
  }
@@ -1616,6 +1529,7 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1616
1529
  } catch (err) {
1617
1530
  logAckFailure({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
1618
1531
  }), channel: "coolclaw", error: err });
1532
+ throw err;
1619
1533
  }
1620
1534
  }
1621
1535
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-TS5TXH6P.js";
4
- import "./chunk-QNBJDZKJ.js";
3
+ } from "./chunk-EE5BK65S.js";
4
+ import "./chunk-QKB2R55C.js";
5
5
 
6
6
  // cli-metadata.ts
7
7
  var cli_metadata_default = index_default;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-TS5TXH6P.js";
4
- import "./chunk-QNBJDZKJ.js";
3
+ } from "./chunk-EE5BK65S.js";
4
+ import "./chunk-QKB2R55C.js";
5
5
  export {
6
6
  index_default as default
7
7
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  coolclawChannelPlugin
3
- } from "./chunk-QNBJDZKJ.js";
3
+ } from "./chunk-QKB2R55C.js";
4
4
 
5
5
  // setup-entry.ts
6
6
  import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coolclaw/coolclaw",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "OpenClaw native channel plugin for Riddle/CoolClaw chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,7 +38,8 @@
38
38
  "scripts": {
39
39
  "build": "tsup index.ts setup-entry.ts cli-metadata.ts --format esm --dts --out-dir dist --clean --splitting",
40
40
  "test": "vitest run",
41
- "lint": "tsc --noEmit"
41
+ "lint": "tsc --noEmit",
42
+ "prepack": "npm run build"
42
43
  },
43
44
  "dependencies": {
44
45
  "ws": "^8.20.1",
@@ -71,7 +72,7 @@
71
72
  "runtimeSetupEntry": "./dist/setup-entry.js",
72
73
  "install": {
73
74
  "npmSpec": "@coolclaw/coolclaw",
74
- "expectedIntegrity": "sha512-imrUEvouY9fCFzZT2GNJBKsuYaJm9boaC6SVKJu5pGXvrDWrYrWmxA/buWiw5ATlJvrIvZZxF3jAk7b9BMnMzA==",
75
+ "expectedIntegrity": "sha512-6q6+KPGtAkHBZGsHgqEXJtnwLdVoZNLrZNbxZmzKisZvItoE2y+Acl9v3bNtqtEK/DR5NLahyloy7FMRVt9Upg==",
75
76
  "defaultChoice": "npm",
76
77
  "minHostVersion": ">=2026.3.22"
77
78
  },