Add gm harness with batch chance tools for play.

Main world layer can loop on the chance tool (requests array) until settlement JSON. Driver forwards output deltas; standalone chance worker accepts batch requests. play_slots.chance gates the harness.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 00:49:19 +08:00
co-authored by Cursor
parent 6b4156fd0f
commit 8360e885bb
9 changed files with 428 additions and 32 deletions
+37 -7
View File
@@ -1,5 +1,5 @@
import type { Blackboard } from "../blackboard/blackboard.js";
import type { LlmProvider, StreamCallbacks } from "../llm/client.js";
import type { LlmProvider, StreamCallbacks, ChatMessage } from "../llm/client.js";
import { supportsContentStream } from "../llm/stream-complete.js";
import { loadWorkerSkillWithContext } from "../skills/loader.js";
import { filterInputsForRolePerspective } from "../skills/worker-llm.js";
@@ -21,6 +21,11 @@ import { resolveActivePreset } from "../preset/store.js";
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
import { PRESENT_TAG } from "../skills/present-packet.js";
import {
gmChanceToolsEnabled,
GM_CHANCE_HARNESS_INSTRUCTION,
} from "../skills/gm-tools.js";
import { runGmHarness } from "./gm-harness.js";
import {
mergeQuestionsPreferFragment,
normalizeQuestions,
@@ -38,6 +43,11 @@ import {
serializeContextOrder,
} from "../skills/context-order.js";
export type WorkerStreamCallbacks = {
onThinkingDelta?: (delta: string) => void;
onOutputDelta?: (delta: string) => void;
};
export type WorkerRunParams = {
skillName: string;
workerId: string;
@@ -49,11 +59,6 @@ export type WorkerRunParams = {
declared?: { worker: ParsedWorkerSkill; promptBody: string };
};
export type WorkerStreamCallbacks = {
onThinkingDelta?: (delta: string) => void;
onOutputDelta?: (delta: string) => void;
};
export type WorkerRunResult = {
outputs: Record<string, string>;
summary: string;
@@ -557,7 +562,19 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
inputs[CONTEXT_BRIEF_TAG] = priorBrief;
}
const systemContent = promptBody + WORKER_OUTPUT_INSTRUCTION;
const useGmHarness =
isPlayLayerActive(params.slots) &&
worker.id === "world-simulator" &&
gmChanceToolsEnabled(
parseWorkerSetYaml(
params.blackboard.getContentByTag("设计.worker集") ?? "",
)?.play_slots,
);
const systemContent =
promptBody +
WORKER_OUTPUT_INSTRUCTION +
(useGmHarness ? GM_CHANCE_HARNESS_INSTRUCTION : "");
const playPreset = isPlayLayerActive(params.slots)
? resolveActivePreset(loadAppSettings().activePresetId)
: null;
@@ -589,6 +606,19 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
},
];
if (useGmHarness) {
const systemMsg = messages.find((m) => m.role === "system");
const rest = messages.filter((m) => m.role !== "system") as ChatMessage[];
const harness = await runGmHarness({
llm: params.llm,
system: systemMsg?.content ?? systemContent,
messages: rest,
stream: params.stream,
caller: `worker:${worker.id}`,
});
return parseWorkerResponse(harness.content, worker.outputTags);
}
const result = await completeWorkerPreferStream(
params.llm,
messages,
+84
View File
@@ -0,0 +1,84 @@
/**
* 主世界层(world-simulator)游玩期 tool harness:
* 反复 completeWithTools 直到不再调用工具,最后一次正文视为裁决 JSON。
*/
import type { ChatMessage, LlmProvider } from "../llm/client.js";
import {
createLocalLlmDriver,
type DriverToolCall,
} from "../runtime/driver.js";
import {
GM_CHANCE_TOOL_NAME,
GM_TOOL_DEFINITIONS,
handleGmToolCall,
} from "../skills/gm-tools.js";
export type GmHarnessStreamCallbacks = {
onThinkingDelta?: (delta: string) => void;
onOutputDelta?: (delta: string) => void;
};
export const DEFAULT_GM_HARNESS_MAX_ITERATIONS = 6;
export type GmHarnessParams = {
llm: LlmProvider;
system: string;
messages: ChatMessage[];
stream?: GmHarnessStreamCallbacks;
maxIterations?: number;
caller?: string;
};
export type GmHarnessResult = {
content: string;
iterations: number;
};
export async function runGmHarness(
params: GmHarnessParams,
): Promise<GmHarnessResult> {
const driver = createLocalLlmDriver(params.llm);
const label = "主世界层 harness";
const result = await driver.run({
system: params.system,
messages: params.messages,
tools: GM_TOOL_DEFINITIONS,
caller: params.caller ?? "worker:world-simulator",
label,
maxIterations: params.maxIterations ?? DEFAULT_GM_HARNESS_MAX_ITERATIONS,
onThinkingDelta: (delta) => params.stream?.onThinkingDelta?.(delta),
onOutputDelta: (delta) => params.stream?.onOutputDelta?.(delta),
onThinkingDone: () => {
/* 思维链由 stream 增量展示 */
},
handleStep: (calls: DriverToolCall[]) => {
const results: Array<{ callId: string; content: string }> = [];
for (const call of calls) {
if (call.name !== GM_CHANCE_TOOL_NAME) {
results.push({
callId: call.id,
content: JSON.stringify({
ok: false,
error: `不允许的工具:${call.name}`,
}),
});
continue;
}
const content =
handleGmToolCall(call.name, call.arguments) ??
JSON.stringify({ ok: false, error: "未知工具" });
results.push({ callId: call.id, content });
}
return { kind: "continue", results };
},
});
if (result.stop.kind === "text") {
return { content: result.stop.content, iterations: result.iterations };
}
throw new Error(
`${label} 以边界 tool 结束(${result.stop.call.name}),主世界层仅允许 ${GM_CHANCE_TOOL_NAME}`,
);
}