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:
@@ -140,6 +140,10 @@ import {
|
|||||||
maintainOpsToTablePatch,
|
maintainOpsToTablePatch,
|
||||||
parseMaintainPacket,
|
parseMaintainPacket,
|
||||||
} from "../skills/maintain-packet.js";
|
} from "../skills/maintain-packet.js";
|
||||||
|
import {
|
||||||
|
MAINTAIN_GENERATE_TAG,
|
||||||
|
runMaintainNeedGenerateSampling,
|
||||||
|
} from "../skills/maintain-need-generate.js";
|
||||||
import {
|
import {
|
||||||
mountResidentContextForWorker as writeResidentContextTags,
|
mountResidentContextForWorker as writeResidentContextTags,
|
||||||
parseResidentContext,
|
parseResidentContext,
|
||||||
@@ -160,6 +164,7 @@ import type { BlackboardItem } from "../types/blackboard.js";
|
|||||||
const PLAY_ROUND_TRANSIENT_TAGS = [
|
const PLAY_ROUND_TRANSIENT_TAGS = [
|
||||||
SETTLEMENT_TAG,
|
SETTLEMENT_TAG,
|
||||||
MAINTAIN_TAG,
|
MAINTAIN_TAG,
|
||||||
|
MAINTAIN_GENERATE_TAG,
|
||||||
"运行.本轮.机遇",
|
"运行.本轮.机遇",
|
||||||
"运行.本轮.变量变更",
|
"运行.本轮.变量变更",
|
||||||
] as const;
|
] as const;
|
||||||
@@ -1633,7 +1638,12 @@ export class PhaseRuntime {
|
|||||||
private applyMaintainTableOps(maintainRaw: string, workerId: string): void {
|
private applyMaintainTableOps(maintainRaw: string, workerId: string): void {
|
||||||
const view = parseMaintainPacket(maintainRaw);
|
const view = parseMaintainPacket(maintainRaw);
|
||||||
const ops = view.packet?.table_ops;
|
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(
|
const current = parseTableDoc(
|
||||||
this.blackboard.getContentByTag("变量.当前"),
|
this.blackboard.getContentByTag("变量.当前"),
|
||||||
);
|
);
|
||||||
@@ -1650,6 +1660,34 @@ export class PhaseRuntime {
|
|||||||
if (keys.length) {
|
if (keys.length) {
|
||||||
this.onMessage(`[旁观维护] 变量.当前 ← ${keys.join("、")}`);
|
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 重复把同一句用户输入追加进历史 */
|
/** 避免 syncSlots 重复把同一句用户输入追加进历史 */
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user