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:
+37
-7
@@ -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,
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user