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,
|
||||
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 重复把同一句用户输入追加进历史 */
|
||||
|
||||
@@ -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