Run need_generate sampling via batch chance in auditor maintain path.

Auditor packets can request generation-rule pools without LLM randomness; results land in 运行.本轮.旁观.生成抽样 and clear each play turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 00:57:51 +08:00
co-authored by Cursor
parent ac474bcd1a
commit 6d4dfd0d89
3 changed files with 184 additions and 1 deletions
+39 -1
View File
@@ -140,6 +140,10 @@ import {
maintainOpsToTablePatch,
parseMaintainPacket,
} from "../skills/maintain-packet.js";
import {
MAINTAIN_GENERATE_TAG,
runMaintainNeedGenerateSampling,
} from "../skills/maintain-need-generate.js";
import {
mountResidentContextForWorker as writeResidentContextTags,
parseResidentContext,
@@ -160,6 +164,7 @@ import type { BlackboardItem } from "../types/blackboard.js";
const PLAY_ROUND_TRANSIENT_TAGS = [
SETTLEMENT_TAG,
MAINTAIN_TAG,
MAINTAIN_GENERATE_TAG,
"运行.本轮.机遇",
"运行.本轮.变量变更",
] as const;
@@ -1633,7 +1638,12 @@ export class PhaseRuntime {
private applyMaintainTableOps(maintainRaw: string, workerId: string): void {
const view = parseMaintainPacket(maintainRaw);
const ops = view.packet?.table_ops;
if (!view.ok || !ops?.length) return;
if (!view.ok || !ops?.length) {
if (view.ok && view.packet?.need_generate) {
this.applyMaintainNeedGenerateSampling(view.packet.need_generate, workerId);
}
return;
}
const current = parseTableDoc(
this.blackboard.getContentByTag("变量.当前"),
);
@@ -1650,6 +1660,34 @@ export class PhaseRuntime {
if (keys.length) {
this.onMessage(`[旁观维护] 变量.当前 ← ${keys.join("、")}`);
}
if (view.packet?.need_generate) {
this.applyMaintainNeedGenerateSampling(view.packet.need_generate, workerId);
}
}
/** need_generate:按生成规则合同程序批量抽样,写入 运行.本轮.旁观.生成抽样 */
private applyMaintainNeedGenerateSampling(
need: { rule_id: string; reason: string },
workerId: string,
): void {
const rulesRaw = this.blackboard.getContentByTag("设计.生成规则");
const result = runMaintainNeedGenerateSampling({
need,
generationRulesRaw: rulesRaw,
});
if (!result) {
this.onMessage(
`[旁观维护] need_generate=${need.rule_id}:未找到可抽样的池(请检查设计.生成规则)`,
);
return;
}
const content = JSON.stringify(
{ rule_id: need.rule_id, reason: need.reason, chance: result },
null,
2,
);
this.writeWorkerTagContent(MAINTAIN_GENERATE_TAG, content, workerId);
this.onMessage(`[旁观维护] 生成抽样 ${need.rule_id}:${result.summary}`);
}
/** 避免 syncSlots 重复把同一句用户输入追加进历史 */
+102
View File
@@ -0,0 +1,102 @@
/**
* 旁观维护 need_generate:按生成规则合同程序抽样(不走 LLM 编随机)。
*/
import {
executeChanceBatch,
type ChanceBatchRequestItem,
type ChanceBatchResult,
} from "./chance-tools.js";
import { extractJsonObjectText } from "./worker-set-parse.js";
import type { MaintainNeedGenerate } from "./maintain-packet.js";
export const MAINTAIN_GENERATE_TAG = "运行.本轮.旁观.生成抽样";
type PoolEntry = { id?: string; 内容?: string; 权重?: number; weight?: number };
function asString(v: unknown): string | undefined {
if (typeof v !== "string") return undefined;
const t = v.trim();
return t || undefined;
}
function findRuleBody(
parsed: Record<string, unknown>,
ruleId: string,
): Record<string, unknown> | undefined {
const body = parsed.正文;
if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
const rules = (body as Record<string, unknown>).rules;
if (!Array.isArray(rules)) return undefined;
for (const row of rules) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const r = row as Record<string, unknown>;
const id =
asString(r.rule_id) ??
asString(r.id) ??
asString(r.规则id);
if (id === ruleId) return r;
}
return undefined;
}
/** 从生成规则产物中为指定 rule_id 构建批量随机请求(每个池一条 pick) */
export function buildChanceBatchFromGenerationRule(
rulesArtifactRaw: string,
ruleId: string,
): ChanceBatchRequestItem[] | null {
const text = extractJsonObjectText(rulesArtifactRaw) ?? rulesArtifactRaw.trim();
if (!text) return null;
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(text) as Record<string, unknown>;
} catch {
return null;
}
const rule = findRuleBody(parsed, ruleId);
if (!rule) return null;
const pools = rule.池 ?? rule.pools;
if (!Array.isArray(pools) || !pools.length) return null;
const requests: ChanceBatchRequestItem[] = [];
for (const poolRaw of pools) {
if (!poolRaw || typeof poolRaw !== "object" || Array.isArray(poolRaw)) continue;
const pool = poolRaw as Record<string, unknown>;
const poolId =
asString(pool.pool_id) ?? asString(pool.池id) ?? asString(pool.名称);
const entries = (pool.条目 ?? pool.entries) as PoolEntry[] | undefined;
if (!poolId || !Array.isArray(entries) || !entries.length) continue;
const items = entries
.map((e, idx) => {
const id = asString(e.id) ?? asString(e.内容) ?? `item-${idx}`;
const weight =
typeof e.权重 === "number"
? e.权重
: typeof e.weight === "number"
? e.weight
: undefined;
return id ? { id, weight } : undefined;
})
.filter((x): x is { id: string; weight?: number } => Boolean(x));
if (!items.length) continue;
requests.push({
id: `pool:${poolId}`,
op: "pick",
items,
count: 1,
unique: true,
reason: `生成规则 ${ruleId} · 池 ${poolId}`,
});
}
return requests.length ? requests : null;
}
export function runMaintainNeedGenerateSampling(params: {
need: Exclude<MaintainNeedGenerate, false>;
generationRulesRaw: string | null | undefined;
}): ChanceBatchResult | null {
const raw = params.generationRulesRaw?.trim();
if (!raw) return null;
const batch = buildChanceBatchFromGenerationRule(raw, params.need.rule_id);
if (!batch?.length) return null;
return executeChanceBatch(batch);
}
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import {
buildChanceBatchFromGenerationRule,
runMaintainNeedGenerateSampling,
} from "../src/skills/maintain-need-generate.js";
const SAMPLE_RULES = JSON.stringify({
schema: "context-fragment.v1",
正文: {
rules: [
{
rule_id: "npc-trait",
池: [
{
pool_id: "mood",
条目: [
{ id: "calm", 权重: 2 },
{ id: "angry", 权重: 1 },
],
},
],
},
],
},
});
describe("maintain need_generate sampling", () => {
it("builds pick requests from generation rule pools", () => {
const batch = buildChanceBatchFromGenerationRule(SAMPLE_RULES, "npc-trait");
expect(batch).toHaveLength(1);
expect(batch![0]!.id).toBe("pool:mood");
expect(batch![0]!.op).toBe("pick");
});
it("runs batch sampling for need_generate", () => {
const result = runMaintainNeedGenerateSampling({
need: { rule_id: "npc-trait", reason: "补一条" },
generationRulesRaw: SAMPLE_RULES,
});
expect(result?.schema).toBe("chance.batch.v1");
expect(result?.results[0]?.id).toBe("pool:mood");
});
});