@coolclaw/coolclaw 1.0.16 → 1.0.17

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.
@@ -180,6 +180,7 @@ var FileAckStore = class {
180
180
 
181
181
  // src/agent-task.ts
182
182
  import { createHash } from "crypto";
183
+ var WEREWOLF_STRUCTURED_ACTION_PROTOCOL_VERSION = "WEREWOLF_STRUCTURED_JSON_MESSAGE_V1";
183
184
  function normalizeAgentTask(value) {
184
185
  if (!isRecord(value)) return null;
185
186
  const actionContract = normalizeActionContract(value.actionContract);
@@ -191,6 +192,9 @@ function normalizeAgentTask(value) {
191
192
  renderedPrompt: readString(value.renderedPrompt) ?? "",
192
193
  renderedPromptHash: readString(value.renderedPromptHash),
193
194
  actionFormat: readString(value.actionFormat),
195
+ actionProtocolVersion: readString(value.actionProtocolVersion),
196
+ outputSchema: isRecord(value.outputSchema) ? value.outputSchema : void 0,
197
+ retryPolicy: normalizeRetryPolicy(value.retryPolicy),
194
198
  conversationKey: readString(value.conversationKey),
195
199
  actionContract,
196
200
  fallbackAction,
@@ -200,6 +204,9 @@ function normalizeAgentTask(value) {
200
204
  function isDispatchableAgentTask(task) {
201
205
  return task.requiresReply === true && task.renderedPrompt.trim().length > 0 && allowedActionTypes(task).length > 0;
202
206
  }
207
+ function isStructuredJsonTask(task) {
208
+ return task.actionProtocolVersion === WEREWOLF_STRUCTURED_ACTION_PROTOCOL_VERSION || task.actionFormat === "STRICT_JSON_OBJECT";
209
+ }
203
210
  function validateAgentAction(action, task) {
204
211
  if (!isRecord(action.actionData)) {
205
212
  return { ok: false, reason: "invalid_action_shape" };
@@ -216,12 +223,18 @@ function validateAgentAction(action, task) {
216
223
  if (!matchesActionDataSchema(repaired.actionData, option.actionDataSchema)) {
217
224
  return { ok: false, reason: "invalid_action_shape" };
218
225
  }
226
+ if (isStructuredJsonTask(task) && !matchesRootOutputSchema(action, task.outputSchema)) {
227
+ return { ok: false, reason: "invalid_action_shape" };
228
+ }
229
+ const normalizedAction = {
230
+ actionType: action.actionType,
231
+ actionData: repaired.actionData
232
+ };
233
+ if (typeof action.speech === "string") normalizedAction.speech = action.speech;
234
+ if (typeof action.reason === "string") normalizedAction.reason = action.reason;
219
235
  return {
220
236
  ok: true,
221
- action: {
222
- actionType: action.actionType,
223
- actionData: repaired.actionData
224
- },
237
+ action: normalizedAction,
225
238
  repairReason: repaired.repairReason
226
239
  };
227
240
  }
@@ -251,6 +264,14 @@ function normalizeActionContract(value) {
251
264
  finalOutputRules: Array.isArray(value.finalOutputRules) ? value.finalOutputRules.filter((rule) => typeof rule === "string") : void 0
252
265
  };
253
266
  }
267
+ function normalizeRetryPolicy(value) {
268
+ if (!isRecord(value)) return void 0;
269
+ return {
270
+ maxRetries: readNumber(value.maxRetries),
271
+ shareDeadline: typeof value.shareDeadline === "boolean" ? value.shareDeadline : void 0,
272
+ rejectedOutputActionType: readString(value.rejectedOutputActionType)
273
+ };
274
+ }
254
275
  function normalizeActionOption(value) {
255
276
  if (!isRecord(value) || typeof value.actionType !== "string" || value.actionType.length === 0) {
256
277
  return [];
@@ -266,20 +287,17 @@ function allowedActionTypes(task) {
266
287
  }
267
288
  function matchesActionDataSchema(actionData, schema) {
268
289
  if (!schema || Object.keys(schema).length === 0) return true;
269
- if (typeof schema.type === "string" && schema.type !== "object") return false;
270
- const properties = isRecord(schema.properties) ? schema.properties : {};
271
- const required = Array.isArray(schema.required) ? schema.required.filter((field) => typeof field === "string" && field.length > 0) : [];
272
- for (const field of required) {
273
- if (!Object.prototype.hasOwnProperty.call(actionData, field)) return false;
274
- if (!matchesSchemaValue(actionData[field], properties[field], true)) return false;
275
- }
276
- for (const [field, value] of Object.entries(actionData)) {
277
- const propertySchema = properties[field];
278
- if (propertySchema !== void 0 && !matchesSchemaValue(value, propertySchema, false)) {
279
- return false;
280
- }
281
- }
282
- return true;
290
+ return matchesSchemaValue(actionData, schema, true);
291
+ }
292
+ function matchesRootOutputSchema(action, schema) {
293
+ if (!schema || Object.keys(schema).length === 0) return true;
294
+ const root = {
295
+ actionType: action.actionType,
296
+ actionData: action.actionData
297
+ };
298
+ if (typeof action.speech === "string") root.speech = action.speech;
299
+ if (typeof action.reason === "string") root.reason = action.reason;
300
+ return matchesSchemaValue(root, schema, true);
283
301
  }
284
302
  function repairActionDataForSchema(actionData, schema) {
285
303
  if (!schema || Object.keys(schema).length === 0) {
@@ -315,12 +333,28 @@ function repairActionDataForSchema(actionData, schema) {
315
333
  };
316
334
  }
317
335
  function matchesSchemaValue(value, schema, required) {
318
- if (value == null) return !required;
319
336
  if (!isRecord(schema)) return true;
337
+ if (value == null) {
338
+ return !required || schemaAllowsType(schema.type, "null");
339
+ }
340
+ const oneOf = Array.isArray(schema.oneOf) ? schema.oneOf : [];
341
+ if (oneOf.length > 0) {
342
+ return oneOf.some((variant) => matchesSchemaValue(value, variant, required));
343
+ }
320
344
  if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
321
345
  return false;
322
346
  }
323
- switch (schema.type) {
347
+ const allowedTypes = Array.isArray(schema.type) ? schema.type.filter((type) => typeof type === "string") : typeof schema.type === "string" ? [schema.type] : [];
348
+ if (allowedTypes.length === 0) {
349
+ return true;
350
+ }
351
+ return allowedTypes.some((type) => matchesSchemaTypedValue(value, schema, type));
352
+ }
353
+ function schemaAllowsType(typeNode, type) {
354
+ return Array.isArray(typeNode) ? typeNode.includes(type) : typeNode === type;
355
+ }
356
+ function matchesSchemaTypedValue(value, schema, type) {
357
+ switch (type) {
324
358
  case "integer":
325
359
  return typeof value === "number" && Number.isInteger(value);
326
360
  case "number":
@@ -330,13 +364,33 @@ function matchesSchemaValue(value, schema, required) {
330
364
  case "boolean":
331
365
  return typeof value === "boolean";
332
366
  case "object":
333
- return isRecord(value);
367
+ return matchesObjectSchema(value, schema);
334
368
  case "array":
335
369
  return Array.isArray(value);
370
+ case "null":
371
+ return value == null;
336
372
  default:
337
373
  return true;
338
374
  }
339
375
  }
376
+ function matchesObjectSchema(value, schema) {
377
+ if (!isRecord(value)) return false;
378
+ const properties = isRecord(schema.properties) ? schema.properties : {};
379
+ const required = Array.isArray(schema.required) ? schema.required.filter((field) => typeof field === "string" && field.length > 0) : [];
380
+ for (const field of required) {
381
+ if (!Object.prototype.hasOwnProperty.call(value, field)) return false;
382
+ if (!matchesSchemaValue(value[field], properties[field], true)) return false;
383
+ }
384
+ for (const [field, fieldValue] of Object.entries(value)) {
385
+ const propertySchema = properties[field];
386
+ if (propertySchema === void 0) {
387
+ if (schema.additionalProperties === false) return false;
388
+ continue;
389
+ }
390
+ if (!matchesSchemaValue(fieldValue, propertySchema, false)) return false;
391
+ }
392
+ return true;
393
+ }
340
394
  function readString(value) {
341
395
  return typeof value === "string" && value.length > 0 ? value : void 0;
342
396
  }
@@ -1047,7 +1101,17 @@ async function sendGameAction(input) {
1047
1101
  rawResponsePreview: input.rawResponsePreview,
1048
1102
  modelActionRejected: input.modelActionRejected,
1049
1103
  modelActionType: input.modelActionType,
1050
- validationReason: input.validationReason
1104
+ validationReason: input.validationReason,
1105
+ submissionStatus: input.submissionStatus,
1106
+ structuredProtocolVersion: input.structuredProtocolVersion,
1107
+ retryCount: input.retryCount,
1108
+ firstRawResponseHash: input.firstRawResponseHash,
1109
+ firstModelActionType: input.firstModelActionType,
1110
+ firstValidationReason: input.firstValidationReason,
1111
+ retryRawResponseHash: input.retryRawResponseHash,
1112
+ retryValidationReason: input.retryValidationReason,
1113
+ speech: input.speech,
1114
+ reason: input.reason
1051
1115
  });
1052
1116
  const response = await input.client.request(frame);
1053
1117
  if (response.ok === false) {
@@ -1123,6 +1187,59 @@ function parseAgentAction(text) {
1123
1187
  }
1124
1188
  return lastError ?? { error: "no_action_block" };
1125
1189
  }
1190
+ function parseStrictJsonAgentAction(text) {
1191
+ if (typeof text !== "string" || text.length === 0) {
1192
+ return { error: "no_action_block" };
1193
+ }
1194
+ const trimmed = text.trim();
1195
+ if (!trimmed.startsWith("{")) {
1196
+ return { error: "invalid_json", detail: "strict_json_object_required" };
1197
+ }
1198
+ const objectEnd = topLevelJsonObjectEnd(trimmed);
1199
+ if (objectEnd === null) {
1200
+ return { error: "invalid_json", detail: "strict_json_object_required" };
1201
+ }
1202
+ if (objectEnd !== trimmed.length - 1) {
1203
+ return { error: "invalid_json", detail: "trailing_text_after_json" };
1204
+ }
1205
+ return parseActionJsonStrict(trimmed);
1206
+ }
1207
+ function topLevelJsonObjectEnd(text) {
1208
+ let depth = 0;
1209
+ let inString = false;
1210
+ let escaped = false;
1211
+ for (let i = 0; i < text.length; i += 1) {
1212
+ const ch = text[i];
1213
+ if (inString) {
1214
+ if (escaped) {
1215
+ escaped = false;
1216
+ } else if (ch === "\\") {
1217
+ escaped = true;
1218
+ } else if (ch === '"') {
1219
+ inString = false;
1220
+ }
1221
+ continue;
1222
+ }
1223
+ if (ch === '"') {
1224
+ inString = true;
1225
+ continue;
1226
+ }
1227
+ if (ch === "{") {
1228
+ depth += 1;
1229
+ continue;
1230
+ }
1231
+ if (ch === "}") {
1232
+ depth -= 1;
1233
+ if (depth === 0) {
1234
+ return i;
1235
+ }
1236
+ if (depth < 0) {
1237
+ return null;
1238
+ }
1239
+ }
1240
+ }
1241
+ return null;
1242
+ }
1126
1243
  function parseActionJson(body) {
1127
1244
  let obj;
1128
1245
  try {
@@ -1151,7 +1268,38 @@ function parseActionJson(body) {
1151
1268
  }
1152
1269
  return {
1153
1270
  actionType: rec.actionType,
1154
- actionData: rec.actionData
1271
+ actionData: rec.actionData,
1272
+ ...typeof rec.speech === "string" ? { speech: rec.speech } : {},
1273
+ ...typeof rec.reason === "string" ? { reason: rec.reason } : {}
1274
+ };
1275
+ }
1276
+ function parseActionJsonStrict(body) {
1277
+ let obj;
1278
+ try {
1279
+ obj = JSON.parse(body);
1280
+ } catch (e) {
1281
+ return { error: "invalid_json", detail: e instanceof Error ? e.message : String(e) };
1282
+ }
1283
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
1284
+ return { error: "invalid_json", detail: "not an object" };
1285
+ }
1286
+ const rec = obj;
1287
+ for (const field of Object.keys(rec)) {
1288
+ if (!["speech", "reason", "actionType", "actionData"].includes(field)) {
1289
+ return { error: "invalid_json", detail: `unknown_top_level_field:${field}` };
1290
+ }
1291
+ }
1292
+ if (typeof rec.actionType !== "string" || rec.actionType.length === 0) {
1293
+ return { error: "missing_action_type" };
1294
+ }
1295
+ if (typeof rec.actionData !== "object" || rec.actionData === null || Array.isArray(rec.actionData)) {
1296
+ return { error: "missing_action_data" };
1297
+ }
1298
+ return {
1299
+ actionType: rec.actionType,
1300
+ actionData: rec.actionData,
1301
+ ...typeof rec.speech === "string" ? { speech: rec.speech } : {},
1302
+ ...typeof rec.reason === "string" ? { reason: rec.reason } : {}
1155
1303
  };
1156
1304
  }
1157
1305
  function removeTrailingCommas(body) {
@@ -1202,7 +1350,7 @@ function inferRejectedModelAction(rawResponse, task) {
1202
1350
  validationReason: "no_model_output"
1203
1351
  };
1204
1352
  }
1205
- const parsed = parseAgentAction(rawResponse);
1353
+ const parsed = isStructuredJsonTask(task) ? parseStrictJsonAgentAction(rawResponse) : parseAgentAction(rawResponse);
1206
1354
  if ("error" in parsed) {
1207
1355
  return {
1208
1356
  modelActionRejected: true,
@@ -1620,11 +1768,13 @@ var CoolclawWsClient = class {
1620
1768
  async connect() {
1621
1769
  this.notifyState("connecting");
1622
1770
  const lastAckedSeq = await this.options.ackStore.getLastAckedSeq(this.options.accountKey);
1771
+ const capabilities = this.options.capabilities ?? [WEREWOLF_STRUCTURED_ACTION_PROTOCOL_VERSION];
1623
1772
  const socket = new WebSocket(buildWsUrl(this.options.gatewayUrl, lastAckedSeq), {
1624
1773
  headers: {
1625
1774
  Authorization: `Bearer ${this.options.token}`,
1626
1775
  "X-CoolClaw-Agent-Id": this.options.agentId,
1627
- "X-CoolClaw-Plugin-Version": this.options.pluginVersion
1776
+ "X-CoolClaw-Plugin-Version": this.options.pluginVersion,
1777
+ "X-CoolClaw-Capabilities": capabilities.join(",")
1628
1778
  }
1629
1779
  });
1630
1780
  this.socket = socket;
@@ -1838,6 +1988,42 @@ function logAckFailure(params) {
1838
1988
  const target = params.target ? ` target=${params.target}` : "";
1839
1989
  params.log(`${params.channel} ack cleanup failed${target}: ${String(params.error)}`);
1840
1990
  }
1991
+ function buildStructuredActionRetryPrompt(renderedPrompt, reason) {
1992
+ const reasonText = reason && reason.trim().length > 0 ? reason.trim() : "invalid_output";
1993
+ return `${renderedPrompt}
1994
+
1995
+ \u4E0A\u4E00\u6B21\u8F93\u51FA\u672A\u88AB\u72FC\u4EBA\u6740\u7ED3\u6784\u5316\u52A8\u4F5C\u534F\u8BAE\u63A5\u53D7\uFF0C\u5931\u8D25\u539F\u56E0\uFF1A${reasonText}\u3002
1996
+ \u8BF7\u91CD\u65B0\u4F5C\u7B54\uFF1A\u53EA\u8F93\u51FA\u4E00\u4E2A\u5B8C\u6574 JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown\u3001\u89E3\u91CA\u6587\u5B57\u3001\u4EE3\u7801\u5757\u6216\u65E7\u52A8\u4F5C\u6807\u7B7E\u3002`;
1997
+ }
1998
+ function hasStructuredRetryBudget(deadlineEpochMs, nowEpochMs = Date.now(), safetyMarginMs = 1e3) {
1999
+ if (!deadlineEpochMs || deadlineEpochMs <= 0) {
2000
+ return true;
2001
+ }
2002
+ return nowEpochMs < deadlineEpochMs - Math.max(0, safetyMarginMs);
2003
+ }
2004
+ function shouldSubmitStructuredRejectedOutput(params) {
2005
+ if (!params.structuredTask || params.gameSubmitted) {
2006
+ return false;
2007
+ }
2008
+ if (params.modelActionRejected === false) {
2009
+ return false;
2010
+ }
2011
+ if (params.fallbackReason?.startsWith("llm_action_submit_failed:")) {
2012
+ return false;
2013
+ }
2014
+ return true;
2015
+ }
2016
+ function flattenForLog(text) {
2017
+ return text.replace(/\r/g, "").replace(/\n/g, " \\n ");
2018
+ }
2019
+ function logStructuredActionValidation(params) {
2020
+ const message = `[COOLCLAW-GAME-ACTION-VALIDATE] gameId=${params.meta.gameId} roomId=${params.meta.roomId} eventType=${params.meta.eventType} eventId=${params.meta.eventId} turnSeq=${params.meta.turnSeq} retryCount=${params.retryCount} submissionStatus=${params.submissionStatus} result=${params.result} reason=${params.reason ?? ""} actionType=${params.actionType ?? ""} promptPolicyVersion=${params.meta.promptPolicyVersion ?? ""} renderedPromptHash=${params.meta.renderedPromptHash ?? ""} rawResponseHash=${params.rawResponseHash ?? ""}`;
2021
+ if (params.result === "pass") {
2022
+ params.log?.info?.(message);
2023
+ } else {
2024
+ params.log?.warn?.(message);
2025
+ }
2026
+ }
1841
2027
  function assertInboundRuntimeAvailable(runtime, context) {
1842
2028
  if (runtime?.channel) {
1843
2029
  return runtime.channel;
@@ -1852,7 +2038,10 @@ async function submitGameActionWithLog(action, meta, wsClient, log, source, rawR
1852
2038
  log?.error?.(`[GAME-ACTION] submit skipped: ws not connected eventId=${meta.eventId}`);
1853
2039
  return "failed";
1854
2040
  }
1855
- const responseHash = rawResponse && rawResponse.length > 0 ? sha256Hex(rawResponse) : void 0;
2041
+ const responseHash = typeof rawResponse === "string" ? sha256Hex(rawResponse) : void 0;
2042
+ const structured = isStructuredJsonTask(meta.agentTask);
2043
+ const retryCount = auditMeta?.retryCount ?? 0;
2044
+ const structuredProtocolVersion = auditMeta?.structuredProtocolVersion ?? meta.agentTask.actionProtocolVersion;
1856
2045
  log?.info?.(
1857
2046
  `[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 ?? ""}`
1858
2047
  );
@@ -1873,10 +2062,20 @@ async function submitGameActionWithLog(action, meta, wsClient, log, source, rawR
1873
2062
  renderedPromptHash: meta.renderedPromptHash,
1874
2063
  parseSource: source,
1875
2064
  rawResponseHash: responseHash,
1876
- rawResponsePreview: rawResponse ? rawResponsePreview(rawResponse) : void 0,
2065
+ rawResponsePreview: typeof rawResponse === "string" ? rawResponsePreview(rawResponse, Number.MAX_SAFE_INTEGER) : void 0,
1877
2066
  modelActionRejected: auditMeta?.modelActionRejected,
1878
2067
  modelActionType: auditMeta?.modelActionType,
1879
- validationReason: normalizeAuditText(auditMeta?.validationReason)
2068
+ validationReason: normalizeAuditText(auditMeta?.validationReason),
2069
+ submissionStatus: auditMeta?.submissionStatus ?? (structured ? "VALID" : void 0),
2070
+ structuredProtocolVersion,
2071
+ retryCount: structured ? retryCount : void 0,
2072
+ firstRawResponseHash: auditMeta?.firstRawResponseHash ?? (structured ? responseHash : void 0),
2073
+ firstModelActionType: auditMeta?.firstModelActionType,
2074
+ firstValidationReason: normalizeAuditText(auditMeta?.firstValidationReason),
2075
+ retryRawResponseHash: auditMeta?.retryRawResponseHash,
2076
+ retryValidationReason: normalizeAuditText(auditMeta?.retryValidationReason),
2077
+ speech: action.speech,
2078
+ reason: action.reason
1880
2079
  });
1881
2080
  if (response.uncertain === true) {
1882
2081
  log?.warn?.(
@@ -1902,6 +2101,128 @@ async function submitGameActionWithLog(action, meta, wsClient, log, source, rawR
1902
2101
  return "failed";
1903
2102
  }
1904
2103
  }
2104
+ async function submitRejectedOutputWithLog(params) {
2105
+ const rawResponse = params.rawResponse ?? "";
2106
+ const rawHash = sha256Hex(rawResponse);
2107
+ const retryCount = params.auditMeta?.retryCount ?? 0;
2108
+ params.log?.warn?.(
2109
+ `[COOLCLAW-GAME-ACTION-VALIDATE] gameId=${params.meta.gameId} roomId=${params.meta.roomId} eventType=${params.meta.eventType} eventId=${params.meta.eventId} turnSeq=${params.meta.turnSeq} retryCount=${retryCount} submissionStatus=REJECTED_OUTPUT result=submit reason=${params.reason} actionType=INVALID_OUTPUT promptPolicyVersion=${params.meta.promptPolicyVersion ?? ""} renderedPromptHash=${params.meta.renderedPromptHash ?? ""} rawResponseHash=${rawHash ?? ""}`
2110
+ );
2111
+ return submitGameActionWithLog(
2112
+ { actionType: "INVALID_OUTPUT", actionData: {} },
2113
+ params.meta,
2114
+ params.wsClient,
2115
+ params.log,
2116
+ "rejected_output",
2117
+ rawResponse,
2118
+ {
2119
+ modelActionRejected: true,
2120
+ validationReason: params.reason,
2121
+ submissionStatus: "REJECTED_OUTPUT",
2122
+ structuredProtocolVersion: params.meta.agentTask.actionProtocolVersion,
2123
+ retryCount,
2124
+ firstRawResponseHash: params.auditMeta?.firstRawResponseHash ?? rawHash,
2125
+ firstModelActionType: params.auditMeta?.firstModelActionType,
2126
+ firstValidationReason: params.auditMeta?.firstValidationReason,
2127
+ retryRawResponseHash: params.auditMeta?.retryRawResponseHash ?? (retryCount > 0 ? rawHash : void 0),
2128
+ retryValidationReason: params.auditMeta?.retryValidationReason ?? (retryCount > 0 ? params.reason : void 0)
2129
+ }
2130
+ );
2131
+ }
2132
+ async function submitStructuredFinalActionWithLog(params) {
2133
+ const rawResponse = params.rawResponse ?? "";
2134
+ const rawHash = sha256Hex(rawResponse);
2135
+ if (rawResponse.trim().length === 0) {
2136
+ logStructuredActionValidation({
2137
+ log: params.log,
2138
+ meta: params.meta,
2139
+ retryCount: params.retryCount,
2140
+ submissionStatus: "REJECTED_OUTPUT",
2141
+ result: "reject",
2142
+ reason: "no_model_output",
2143
+ rawResponseHash: rawHash
2144
+ });
2145
+ return { submitted: false, reason: "no_model_output", rawHash };
2146
+ }
2147
+ const parsed = parseStrictJsonAgentAction(rawResponse);
2148
+ if ("error" in parsed) {
2149
+ const reason = parsed.error === "invalid_json" ? `invalid_json:${parsed.detail}` : parsed.error;
2150
+ logStructuredActionValidation({
2151
+ log: params.log,
2152
+ meta: params.meta,
2153
+ retryCount: params.retryCount,
2154
+ submissionStatus: "REJECTED_OUTPUT",
2155
+ result: "reject",
2156
+ reason,
2157
+ rawResponseHash: rawHash
2158
+ });
2159
+ return {
2160
+ submitted: false,
2161
+ reason,
2162
+ rawHash
2163
+ };
2164
+ }
2165
+ const validation = validateAgentAction(parsed, params.meta.agentTask);
2166
+ if (!validation.ok) {
2167
+ logStructuredActionValidation({
2168
+ log: params.log,
2169
+ meta: params.meta,
2170
+ retryCount: params.retryCount,
2171
+ submissionStatus: "REJECTED_OUTPUT",
2172
+ result: "reject",
2173
+ reason: validation.reason,
2174
+ actionType: parsed.actionType,
2175
+ rawResponseHash: rawHash
2176
+ });
2177
+ return {
2178
+ submitted: false,
2179
+ reason: validation.reason,
2180
+ rawHash,
2181
+ modelActionType: parsed.actionType
2182
+ };
2183
+ }
2184
+ logStructuredActionValidation({
2185
+ log: params.log,
2186
+ meta: params.meta,
2187
+ retryCount: params.retryCount,
2188
+ submissionStatus: "VALID",
2189
+ result: "pass",
2190
+ reason: validation.repairReason,
2191
+ actionType: validation.action.actionType,
2192
+ rawResponseHash: rawHash
2193
+ });
2194
+ const status = await submitGameActionWithLog(
2195
+ validation.action,
2196
+ params.meta,
2197
+ params.wsClient,
2198
+ params.log,
2199
+ "llm",
2200
+ rawResponse,
2201
+ {
2202
+ ...params.auditMeta,
2203
+ modelActionRejected: false,
2204
+ modelActionType: validation.action.actionType,
2205
+ validationReason: validation.repairReason,
2206
+ submissionStatus: "VALID",
2207
+ structuredProtocolVersion: params.meta.agentTask.actionProtocolVersion,
2208
+ retryCount: params.retryCount,
2209
+ firstRawResponseHash: params.auditMeta?.firstRawResponseHash ?? rawHash,
2210
+ firstModelActionType: params.auditMeta?.firstModelActionType,
2211
+ firstValidationReason: params.auditMeta?.firstValidationReason,
2212
+ retryRawResponseHash: params.retryCount > 0 ? params.auditMeta?.retryRawResponseHash ?? rawHash : params.auditMeta?.retryRawResponseHash,
2213
+ retryValidationReason: params.retryCount > 0 ? params.auditMeta?.retryValidationReason ?? validation.repairReason : params.auditMeta?.retryValidationReason
2214
+ }
2215
+ );
2216
+ return {
2217
+ submitted: status === "submitted" || status === "uncertain",
2218
+ status,
2219
+ reason: status === "failed" ? `llm_action_submit_failed:${validation.action.actionType}` : void 0,
2220
+ rawHash,
2221
+ modelActionType: validation.action.actionType,
2222
+ validationReason: validation.repairReason,
2223
+ validAction: true
2224
+ };
2225
+ }
1905
2226
  async function submitBackendFallbackWithLog(params) {
1906
2227
  const fb = backendFallbackAction(params.meta.agentTask);
1907
2228
  if (!fb) {
@@ -2114,6 +2435,14 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2114
2435
  let gameModelActionType;
2115
2436
  let gameValidationReason;
2116
2437
  let gameModelActionRejected;
2438
+ let gameReplyAttempt = 0;
2439
+ let gameFirstRawResponseHash;
2440
+ let gameFirstModelActionType;
2441
+ let gameFirstValidationReason;
2442
+ let gameRetryRawResponseHash;
2443
+ let gameRetryValidationReason;
2444
+ let gameBaseReplyContext = null;
2445
+ let dispatchGameReply = null;
2117
2446
  const gameBuffer = [];
2118
2447
  const modelQueryCollector = modelQueryMeta ? createArenaModelQueryReplyCollector({
2119
2448
  meta: modelQueryMeta,
@@ -2163,6 +2492,23 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2163
2492
  logInboundDrop({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
2164
2493
  }), channel: productFlavor.channelId, reason: "runtime not available; skipping dispatch" });
2165
2494
  if (isGameEvent && gameMeta) {
2495
+ if (isStructuredJsonTask(gameMeta.agentTask)) {
2496
+ const submitted2 = await submitRejectedOutputWithLog({
2497
+ meta: gameMeta,
2498
+ wsClient,
2499
+ log: ctx.log,
2500
+ reason: "runtime_not_available",
2501
+ rawResponse: "",
2502
+ auditMeta: {
2503
+ retryCount: 0,
2504
+ firstValidationReason: "runtime_not_available"
2505
+ }
2506
+ });
2507
+ if (submitted2 === "failed") {
2508
+ throw new Error("game rejected-output submit failed: runtime_not_available");
2509
+ }
2510
+ return;
2511
+ }
2166
2512
  const submitted = await submitBackendFallbackWithLog({
2167
2513
  meta: gameMeta,
2168
2514
  wsClient,
@@ -2262,6 +2608,7 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2262
2608
  WasMentioned: envelope.shouldReply,
2263
2609
  Mentioned: envelope.shouldReply
2264
2610
  });
2611
+ gameBaseReplyContext = ctxPayload;
2265
2612
  const sessionKey = ctxPayload.SessionKey ?? route.sessionKey;
2266
2613
  const mainSessionKey = route.mainSessionKey;
2267
2614
  await runtimeChannel.session.recordInboundSession({
@@ -2273,8 +2620,8 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2273
2620
  ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
2274
2621
  }
2275
2622
  });
2276
- await runtimeChannel.reply.dispatchReplyWithBufferedBlockDispatcher({
2277
- ctx: ctxPayload,
2623
+ dispatchGameReply = async (replyCtxPayload) => runtimeChannel.reply.dispatchReplyWithBufferedBlockDispatcher({
2624
+ ctx: replyCtxPayload,
2278
2625
  cfg: ctx.cfg,
2279
2626
  // 群聊强制走 automatic:CoolClaw 群聊业务语义就是 @ 即回,
2280
2627
  // 而 OpenClaw 默认对 group/channel 走 message_tool_only,
@@ -2298,8 +2645,10 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2298
2645
  if (isGameEvent && gameMeta) {
2299
2646
  if (gameSubmitted) return;
2300
2647
  gameBuffer.push(String(payload.text));
2648
+ const structuredTask = isStructuredJsonTask(gameMeta.agentTask);
2649
+ if (structuredTask) return;
2301
2650
  const full = gameBuffer.join("");
2302
- const parsed = parseAgentAction(full);
2651
+ const parsed = structuredTask ? parseStrictJsonAgentAction(full) : parseAgentAction(full);
2303
2652
  if ("error" in parsed) return;
2304
2653
  const validation = validateAgentAction(parsed, gameMeta.agentTask);
2305
2654
  if (!validation.ok) {
@@ -2307,11 +2656,30 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2307
2656
  gameModelActionType = parsed.actionType;
2308
2657
  gameValidationReason = validation.reason;
2309
2658
  gameFallbackReason = validation.reason;
2659
+ if (structuredTask) {
2660
+ const rawHash2 = sha256Hex(full);
2661
+ if (gameReplyAttempt === 0) {
2662
+ gameFirstRawResponseHash = rawHash2;
2663
+ gameFirstModelActionType = parsed.actionType;
2664
+ gameFirstValidationReason = validation.reason;
2665
+ } else {
2666
+ gameRetryRawResponseHash = rawHash2;
2667
+ gameRetryValidationReason = validation.reason;
2668
+ }
2669
+ }
2310
2670
  ctx.log?.warn?.(
2311
- `[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)}`
2671
+ `[COOLCLAW-GAME-ACTION-VALIDATE] gameId=${gameMeta.gameId} roomId=${gameMeta.roomId} eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} turnSeq=${gameMeta.turnSeq} retryCount=${gameReplyAttempt} submissionStatus=REJECTED_OUTPUT result=reject reason=${validation.reason} actionType=${parsed.actionType} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawResponseHash=${sha256Hex(full)}`
2312
2672
  );
2313
2673
  return;
2314
2674
  }
2675
+ if (structuredTask) {
2676
+ gameModelActionRejected = false;
2677
+ gameModelActionType = validation.action.actionType;
2678
+ gameValidationReason = validation.repairReason;
2679
+ gameFallbackReason = null;
2680
+ return;
2681
+ }
2682
+ const rawHash = sha256Hex(full);
2315
2683
  const submitted = await submitGameActionWithLog(
2316
2684
  validation.action,
2317
2685
  gameMeta,
@@ -2322,7 +2690,15 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2322
2690
  {
2323
2691
  modelActionRejected: false,
2324
2692
  modelActionType: validation.action.actionType,
2325
- validationReason: validation.repairReason
2693
+ validationReason: validation.repairReason,
2694
+ submissionStatus: structuredTask ? "VALID" : void 0,
2695
+ structuredProtocolVersion: structuredTask ? gameMeta.agentTask.actionProtocolVersion : void 0,
2696
+ retryCount: structuredTask ? gameReplyAttempt : void 0,
2697
+ firstRawResponseHash: structuredTask ? gameFirstRawResponseHash ?? rawHash : void 0,
2698
+ firstModelActionType: structuredTask ? gameFirstModelActionType : void 0,
2699
+ firstValidationReason: structuredTask ? gameFirstValidationReason : void 0,
2700
+ retryRawResponseHash: structuredTask && gameReplyAttempt > 0 ? rawHash : void 0,
2701
+ retryValidationReason: structuredTask && gameReplyAttempt > 0 ? validation.repairReason : void 0
2326
2702
  }
2327
2703
  );
2328
2704
  if (submitted === "submitted" || submitted === "uncertain") {
@@ -2361,6 +2737,7 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2361
2737
  }
2362
2738
  }
2363
2739
  });
2740
+ await dispatchGameReply(ctxPayload);
2364
2741
  if (isGameEvent && gameMeta && !gameSubmitted) {
2365
2742
  if (!gameFallbackReason) {
2366
2743
  gameFallbackReason = "no_valid_action_in_llm_output";
@@ -2392,10 +2769,91 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2392
2769
  gameFallbackReason = `dispatch_error: ${errMsg}`;
2393
2770
  }
2394
2771
  } finally {
2772
+ const submitFinalStructuredGameAction = async () => {
2773
+ if (!isGameEvent || !gameMeta || gameSubmitted || !isStructuredJsonTask(gameMeta.agentTask)) {
2774
+ return false;
2775
+ }
2776
+ const rawResponse = gameBuffer.join("");
2777
+ const result = await submitStructuredFinalActionWithLog({
2778
+ rawResponse,
2779
+ meta: gameMeta,
2780
+ wsClient,
2781
+ log: ctx.log,
2782
+ retryCount: gameReplyAttempt,
2783
+ auditMeta: {
2784
+ retryCount: gameReplyAttempt,
2785
+ firstRawResponseHash: gameFirstRawResponseHash,
2786
+ firstModelActionType: gameFirstModelActionType,
2787
+ firstValidationReason: gameFirstValidationReason,
2788
+ retryRawResponseHash: gameRetryRawResponseHash,
2789
+ retryValidationReason: gameRetryValidationReason
2790
+ }
2791
+ });
2792
+ if (result.submitted) {
2793
+ gameSubmitted = true;
2794
+ gameModelActionRejected = false;
2795
+ gameModelActionType = result.modelActionType;
2796
+ gameValidationReason = result.validationReason;
2797
+ gameFallbackReason = null;
2798
+ return true;
2799
+ }
2800
+ gameModelActionRejected = result.validAction ? false : true;
2801
+ gameModelActionType = result.modelActionType;
2802
+ gameValidationReason = result.reason ?? result.validationReason;
2803
+ gameFallbackReason = result.reason ?? gameFallbackReason ?? "invalid_output";
2804
+ if (rawResponse.trim().length > 0) {
2805
+ const rawHash = result.rawHash ?? sha256Hex(rawResponse);
2806
+ if (gameReplyAttempt === 0) {
2807
+ gameFirstRawResponseHash = gameFirstRawResponseHash ?? rawHash;
2808
+ gameFirstModelActionType = gameFirstModelActionType ?? result.modelActionType;
2809
+ gameFirstValidationReason = gameFirstValidationReason ?? gameValidationReason;
2810
+ } else {
2811
+ gameRetryRawResponseHash = gameRetryRawResponseHash ?? rawHash;
2812
+ gameRetryValidationReason = gameRetryValidationReason ?? gameValidationReason;
2813
+ }
2814
+ }
2815
+ return false;
2816
+ };
2817
+ await submitFinalStructuredGameAction();
2818
+ if (isGameEvent && gameMeta && !gameSubmitted && gameReplyAttempt === 0 && isStructuredJsonTask(gameMeta.agentTask) && dispatchGameReply && gameBaseReplyContext && hasStructuredRetryBudget(gameMeta.deadlineEpochMs) && Math.max(0, gameMeta.agentTask.retryPolicy?.maxRetries ?? 0) > 0) {
2819
+ const firstRawResponse = gameBuffer.join("");
2820
+ if (firstRawResponse.trim().length > 0) {
2821
+ const firstInferred = inferRejectedModelAction(firstRawResponse, gameMeta.agentTask);
2822
+ if (gameModelActionRejected === false || firstInferred.validationReason === "valid_action_not_submitted") {
2823
+ ctx.log?.warn?.(
2824
+ `[GAME-ACTION] structured retry skipped for valid unsubmitted action eventId=${gameMeta.eventId} turnSeq=${gameMeta.turnSeq} reason=${gameFallbackReason ?? firstInferred.validationReason ?? "submit_failed"}`
2825
+ );
2826
+ } else {
2827
+ const firstRawHash = sha256Hex(firstRawResponse);
2828
+ gameFirstRawResponseHash = gameFirstRawResponseHash ?? firstRawHash;
2829
+ gameFirstModelActionType = gameFirstModelActionType ?? firstInferred.modelActionType;
2830
+ gameFirstValidationReason = gameFirstValidationReason ?? gameValidationReason ?? firstInferred.validationReason;
2831
+ const retryReason = gameFirstValidationReason ?? firstInferred.validationReason ?? "invalid_output";
2832
+ ctx.log?.warn?.(
2833
+ `[GAME-ACTION] structured retry start eventId=${gameMeta.eventId} turnSeq=${gameMeta.turnSeq} reason=${retryReason} firstRawResponseHash=${firstRawHash}`
2834
+ );
2835
+ gameReplyAttempt = 1;
2836
+ gameBuffer.length = 0;
2837
+ gameFallbackReason = null;
2838
+ gameModelActionRejected = void 0;
2839
+ gameModelActionType = void 0;
2840
+ gameValidationReason = void 0;
2841
+ const retryPrompt = buildStructuredActionRetryPrompt(envelope.text, retryReason);
2842
+ await dispatchGameReply({
2843
+ ...gameBaseReplyContext,
2844
+ Body: retryPrompt,
2845
+ BodyForAgent: retryPrompt,
2846
+ RawBody: retryPrompt,
2847
+ CommandBody: retryPrompt
2848
+ });
2849
+ await submitFinalStructuredGameAction();
2850
+ }
2851
+ }
2852
+ }
2395
2853
  if (isGameEvent && gameMeta) {
2396
2854
  const rawResponse = gameBuffer.join("");
2397
2855
  ctx.log?.info?.(
2398
- `[GAME-TASK] model-output eventId=${gameMeta.eventId} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawHash=${rawResponse ? sha256Hex(rawResponse) : ""} rawPreview=${rawResponsePreview(rawResponse) ?? ""}`
2856
+ `[GAME-TASK] model-output eventId=${gameMeta.eventId} turnSeq=${gameMeta.turnSeq} retryAttempt=${gameReplyAttempt} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawHash=${rawResponse ? sha256Hex(rawResponse) : ""} rawResponseText=${rawResponse ? flattenForLog(rawResponse) : ""}`
2399
2857
  );
2400
2858
  }
2401
2859
  if (isGameEvent && gameMeta && !gameSubmitted) {
@@ -2408,6 +2866,55 @@ var coolclawChannelPlugin = createChatChannelPlugin({
2408
2866
  if (!gameFallbackReason || gameFallbackReason === "no_valid_action_in_llm_output") {
2409
2867
  gameFallbackReason = gameValidationReason ?? inferred.validationReason ?? gameFallbackReason;
2410
2868
  }
2869
+ if (isStructuredJsonTask(gameMeta.agentTask)) {
2870
+ if (rawResponse.trim().length === 0) {
2871
+ gameFallbackReason = "no_model_output";
2872
+ gameValidationReason = gameValidationReason ?? "no_model_output";
2873
+ }
2874
+ if (gameReplyAttempt > 0 && rawResponse.trim().length > 0) {
2875
+ gameRetryRawResponseHash = gameRetryRawResponseHash ?? sha256Hex(rawResponse);
2876
+ gameRetryValidationReason = gameRetryValidationReason ?? gameValidationReason ?? inferred.validationReason;
2877
+ }
2878
+ const rawHashForAudit = rawResponse.trim().length > 0 ? sha256Hex(rawResponse) : void 0;
2879
+ const rejectedOutputReason = gameFallbackReason ?? gameValidationReason ?? inferred.validationReason ?? "invalid_json";
2880
+ if (!shouldSubmitStructuredRejectedOutput({
2881
+ structuredTask: true,
2882
+ gameSubmitted,
2883
+ modelActionRejected: gameModelActionRejected,
2884
+ fallbackReason: rejectedOutputReason
2885
+ })) {
2886
+ logStructuredActionValidation({
2887
+ log: ctx.log,
2888
+ meta: gameMeta,
2889
+ retryCount: gameReplyAttempt,
2890
+ submissionStatus: "VALID",
2891
+ result: "submit_failed_no_rejected_output",
2892
+ reason: rejectedOutputReason,
2893
+ actionType: gameModelActionType,
2894
+ rawResponseHash: rawHashForAudit
2895
+ });
2896
+ return;
2897
+ }
2898
+ const submitted2 = await submitRejectedOutputWithLog({
2899
+ meta: gameMeta,
2900
+ wsClient,
2901
+ log: ctx.log,
2902
+ reason: rejectedOutputReason,
2903
+ rawResponse,
2904
+ auditMeta: {
2905
+ retryCount: gameReplyAttempt,
2906
+ firstRawResponseHash: gameFirstRawResponseHash ?? rawHashForAudit,
2907
+ firstModelActionType: gameFirstModelActionType ?? gameModelActionType,
2908
+ firstValidationReason: gameFirstValidationReason ?? gameValidationReason ?? inferred.validationReason,
2909
+ retryRawResponseHash: gameRetryRawResponseHash,
2910
+ retryValidationReason: gameRetryValidationReason
2911
+ }
2912
+ });
2913
+ if (submitted2 === "failed") {
2914
+ throw new Error(`game rejected-output submit failed: ${gameFallbackReason ?? "unknown"}`);
2915
+ }
2916
+ return;
2917
+ }
2411
2918
  const submitted = await submitBackendFallbackWithLog({
2412
2919
  meta: gameMeta,
2413
2920
  wsClient,
@@ -4,7 +4,7 @@ import {
4
4
  coolclawChannelPlugin,
5
5
  defaultBindingFile,
6
6
  setCoolclawRuntime
7
- } from "./chunk-BVFSS5UA.js";
7
+ } from "./chunk-3YKMAM6K.js";
8
8
 
9
9
  // index.ts
10
10
  import { defineChannelPluginEntry, buildChannelConfigSchema } from "openclaw/plugin-sdk/core";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-TCWYIFNP.js";
4
- import "./chunk-BVFSS5UA.js";
3
+ } from "./chunk-RPT3G66A.js";
4
+ import "./chunk-3YKMAM6K.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-TCWYIFNP.js";
4
- import "./chunk-BVFSS5UA.js";
3
+ } from "./chunk-RPT3G66A.js";
4
+ import "./chunk-3YKMAM6K.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-BVFSS5UA.js";
3
+ } from "./chunk-3YKMAM6K.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.16",
3
+ "version": "1.0.17",
4
4
  "description": "OpenClaw native channel plugin for CoolClaw chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",