引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { BlackboardItem } from "../types/blackboard.js";
|
||||
import type { RuntimeSession } from "../types/runtime.js";
|
||||
import { SLOT_PLAY_TURN_QUEUE, SLOT_PLAY_LAYER_ACTIVE } from "../skills/play-turn.js";
|
||||
|
||||
/**
|
||||
* 判断黑板 tag 是否属于 run 阶段(非实例化确认稿)。
|
||||
@@ -12,9 +13,14 @@ export function isRunPhaseBlackboardTag(tag: string): boolean {
|
||||
if (/^book\./.test(tag)) return false;
|
||||
if (/^情境\./.test(tag)) return false;
|
||||
if (/^博弈\./.test(tag)) return false;
|
||||
// 锁定开局:跟规格一起进 instance
|
||||
if (tag === "输出.开场白" || tag === "运行.初始变量" || tag === "变量.当前") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// run 层:剥离
|
||||
// run 层:剥离(含聊天历史,不写进角色卡)
|
||||
return (
|
||||
tag === "对话.历史" ||
|
||||
/^运行\./.test(tag) ||
|
||||
/^世界\.(当前|裁决)/.test(tag) ||
|
||||
/^场景\.公开/.test(tag) ||
|
||||
@@ -35,6 +41,11 @@ export function prepareRuntimeSessionForInstance(session: RuntimeSession): Runti
|
||||
copy.pendingDecision = undefined;
|
||||
copy.currentWorkerId = undefined;
|
||||
copy.waitingReason = undefined;
|
||||
copy.slots = {
|
||||
...copy.slots,
|
||||
[SLOT_PLAY_TURN_QUEUE]: undefined,
|
||||
[SLOT_PLAY_LAYER_ACTIVE]: undefined,
|
||||
};
|
||||
copy.artifacts = copy.artifacts.filter((a) =>
|
||||
a.outputTags.every((tag) => !isRunPhaseBlackboardTag(tag)),
|
||||
);
|
||||
|
||||
+62
-45
@@ -1,6 +1,10 @@
|
||||
import type { LlmConfig } from "../config/env.js";
|
||||
import type { GenerationParameters } from "../types/preset.js";
|
||||
import { consumeOpenAiToolStream } from "./stream-complete.js";
|
||||
import {
|
||||
currentAbortSignal,
|
||||
throwIfAborted,
|
||||
} from "./run-abort.js";
|
||||
|
||||
export type ToolCallPayload = {
|
||||
id: string;
|
||||
@@ -58,6 +62,8 @@ export type CompleteOptions = {
|
||||
generation?: GenerationParameters;
|
||||
/** 统计用途,如 main_agent / worker:write-rules */
|
||||
caller?: string;
|
||||
/** 取消进行中的 LLM 请求(停止并重试) */
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type CompleteWithToolsOptions = CompleteOptions & {
|
||||
@@ -237,6 +243,24 @@ function extractMessageParts(message: Record<string, unknown> | undefined): {
|
||||
return { content: "", toolCalls };
|
||||
}
|
||||
|
||||
async function llmFetch(
|
||||
url: string,
|
||||
apiKey: string,
|
||||
body: unknown,
|
||||
options?: CompleteOptions,
|
||||
): Promise<Response> {
|
||||
throwIfAborted(options?.signal);
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: options?.signal ?? currentAbortSignal(),
|
||||
});
|
||||
}
|
||||
|
||||
export class OpenAiCompatibleProvider implements LlmProvider {
|
||||
constructor(private readonly config: LlmConfig) {}
|
||||
|
||||
@@ -245,14 +269,12 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
||||
options?: CompleteOptions,
|
||||
): Promise<CompleteResult> {
|
||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(buildRequestBody(this.config, messages, options)),
|
||||
});
|
||||
const response = await llmFetch(
|
||||
url,
|
||||
this.config.apiKey,
|
||||
buildRequestBody(this.config, messages, options),
|
||||
options,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
@@ -282,16 +304,12 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
||||
callbacks: StreamCallbacks = {},
|
||||
): Promise<CompleteResult> {
|
||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(
|
||||
buildRequestBody(this.config, messages, { ...options, stream: true }),
|
||||
),
|
||||
});
|
||||
const response = await llmFetch(
|
||||
url,
|
||||
this.config.apiKey,
|
||||
buildRequestBody(this.config, messages, { ...options, stream: true }),
|
||||
options,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
@@ -319,19 +337,15 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
||||
options: CompleteWithToolsOptions,
|
||||
): Promise<CompleteWithToolsResult> {
|
||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(
|
||||
buildRequestBody(this.config, messages, {
|
||||
...options,
|
||||
tools: options.tools,
|
||||
}),
|
||||
),
|
||||
});
|
||||
const response = await llmFetch(
|
||||
url,
|
||||
this.config.apiKey,
|
||||
buildRequestBody(this.config, messages, {
|
||||
...options,
|
||||
tools: options.tools,
|
||||
}),
|
||||
options,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
@@ -362,20 +376,16 @@ export class OpenAiCompatibleProvider implements LlmProvider {
|
||||
callbacks: StreamCallbacks,
|
||||
): Promise<CompleteWithToolsResult> {
|
||||
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(
|
||||
buildRequestBody(this.config, messages, {
|
||||
...options,
|
||||
tools: options.tools,
|
||||
stream: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
const response = await llmFetch(
|
||||
url,
|
||||
this.config.apiKey,
|
||||
buildRequestBody(this.config, messages, {
|
||||
...options,
|
||||
tools: options.tools,
|
||||
stream: true,
|
||||
}),
|
||||
options,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
@@ -425,6 +435,7 @@ export class MockLlmProvider implements LlmProvider {
|
||||
_messages: ChatMessage[],
|
||||
options?: CompleteOptions,
|
||||
): Promise<CompleteResult> {
|
||||
throwIfAborted(options?.signal);
|
||||
const step = this.nextStep();
|
||||
const response =
|
||||
typeof step === "string"
|
||||
@@ -448,6 +459,7 @@ export class MockLlmProvider implements LlmProvider {
|
||||
options?: CompleteOptions,
|
||||
callbacks: StreamCallbacks = {},
|
||||
): Promise<CompleteResult> {
|
||||
throwIfAborted(options?.signal);
|
||||
const step = this.nextStep();
|
||||
const response =
|
||||
typeof step === "string"
|
||||
@@ -455,10 +467,12 @@ export class MockLlmProvider implements LlmProvider {
|
||||
: (step.content ?? JSON.stringify({ action: "ask_user", reason: "mock" }));
|
||||
const reasoning = "用户需要明确分工 → 调用 design-intake 产出 worker 集。";
|
||||
for (const ch of reasoning) {
|
||||
throwIfAborted(options?.signal);
|
||||
callbacks.onReasoningDelta?.(ch);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
for (const ch of response) {
|
||||
throwIfAborted(options?.signal);
|
||||
callbacks.onContentDelta?.(ch);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
@@ -480,6 +494,7 @@ export class MockLlmProvider implements LlmProvider {
|
||||
_messages: ChatMessage[],
|
||||
_options: CompleteWithToolsOptions,
|
||||
): Promise<CompleteWithToolsResult> {
|
||||
throwIfAborted(_options?.signal);
|
||||
const step = this.nextStep();
|
||||
if (typeof step === "string") {
|
||||
return {
|
||||
@@ -507,9 +522,11 @@ export class MockLlmProvider implements LlmProvider {
|
||||
_options: CompleteWithToolsOptions,
|
||||
callbacks: StreamCallbacks,
|
||||
): Promise<CompleteWithToolsResult> {
|
||||
throwIfAborted(_options?.signal);
|
||||
const reasoning =
|
||||
"用户需要明确 Worker 分工 → 先读取黑板与 worker 列表 → 调用 design-intake 产出 worker 集。";
|
||||
for (const ch of reasoning) {
|
||||
throwIfAborted(_options?.signal);
|
||||
callbacks.onReasoningDelta?.(ch);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
|
||||
+74
-34
@@ -1,4 +1,5 @@
|
||||
import { assemblePresetMessages, mergeMessages } from "../preset/assembler.js";
|
||||
import { loadAppSettings } from "../config/settings.js";
|
||||
import { resolveActivePreset } from "../preset/store.js";
|
||||
import type { PresetPackage } from "../types/preset.js";
|
||||
import type {
|
||||
ChatMessage,
|
||||
@@ -9,9 +10,35 @@ import type {
|
||||
LlmProvider,
|
||||
StreamCallbacks,
|
||||
} from "./client.js";
|
||||
import {
|
||||
TokenTrackingProvider,
|
||||
type LlmTrackingContext,
|
||||
} from "./token-tracker.js";
|
||||
|
||||
export type LlmTrackingRef = { current: LlmTrackingContext };
|
||||
|
||||
function readActivePreset(): PresetPackage | null {
|
||||
return resolveActivePreset(loadAppSettings().activePresetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在所有 LLM 请求前注入当前 preset 的 prompt 片段与生成参数。
|
||||
* 统计在内、预设在外:痕迹里的 messages / generation 就是发给模型的那一份。
|
||||
* 当前没有选用预设时仍包一层,请求原样转发。
|
||||
*/
|
||||
export function wrapLlmForSession(
|
||||
inner: LlmProvider,
|
||||
trackingRef?: LlmTrackingRef,
|
||||
getPreset: () => PresetPackage | null = readActivePreset,
|
||||
): LlmProvider {
|
||||
const tracked = trackingRef
|
||||
? new TokenTrackingProvider(inner, () => trackingRef.current)
|
||||
: inner;
|
||||
return new PresetLlmProvider(tracked, getPreset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在所有 LLM 请求上合并当前 preset 的生成参数。
|
||||
* prompt / marker 不在此前置,由 play worker 按扩展 prompt_order 填洞。
|
||||
*/
|
||||
export class PresetLlmProvider implements LlmProvider {
|
||||
constructor(
|
||||
@@ -19,41 +46,57 @@ export class PresetLlmProvider implements LlmProvider {
|
||||
private readonly getPreset: () => PresetPackage | null,
|
||||
) {}
|
||||
|
||||
private prepare(
|
||||
messages: ChatMessage[],
|
||||
options?: CompleteOptions,
|
||||
): { messages: ChatMessage[]; options: CompleteOptions | undefined } {
|
||||
const preset = this.getPreset();
|
||||
if (!preset) return { messages, options };
|
||||
// 生成参数全局合并;prompt/marker 只在 play worker 按 prompt_order 填洞,这里不再前置。
|
||||
return {
|
||||
messages,
|
||||
options: {
|
||||
...options,
|
||||
generation: options?.generation ?? preset.generation,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async complete(
|
||||
messages: ChatMessage[],
|
||||
options?: CompleteOptions,
|
||||
): Promise<CompleteResult> {
|
||||
const preset = this.getPreset();
|
||||
if (!preset) {
|
||||
return this.inner.complete(messages, options);
|
||||
const prepared = this.prepare(messages, options);
|
||||
return this.inner.complete(prepared.messages, prepared.options);
|
||||
}
|
||||
|
||||
async completeStream(
|
||||
messages: ChatMessage[],
|
||||
options?: CompleteOptions,
|
||||
callbacks: StreamCallbacks = {},
|
||||
): Promise<CompleteResult> {
|
||||
if (!this.inner.completeStream) {
|
||||
const result = await this.complete(messages, options);
|
||||
if (result.reasoning) callbacks.onReasoningDelta?.(result.reasoning);
|
||||
if (result.content) callbacks.onContentDelta?.(result.content);
|
||||
return result;
|
||||
}
|
||||
|
||||
const presetMessages = assemblePresetMessages(preset);
|
||||
const merged = mergeMessages(presetMessages, messages);
|
||||
const generation = options?.generation ?? preset.generation;
|
||||
|
||||
return this.inner.complete(merged, {
|
||||
...options,
|
||||
generation,
|
||||
});
|
||||
const prepared = this.prepare(messages, options);
|
||||
return this.inner.completeStream(
|
||||
prepared.messages,
|
||||
prepared.options,
|
||||
callbacks,
|
||||
);
|
||||
}
|
||||
|
||||
async completeWithTools(
|
||||
messages: ChatMessage[],
|
||||
options: CompleteWithToolsOptions,
|
||||
): Promise<CompleteWithToolsResult> {
|
||||
const preset = this.getPreset();
|
||||
if (!preset) {
|
||||
return this.inner.completeWithTools(messages, options);
|
||||
}
|
||||
|
||||
const presetMessages = assemblePresetMessages(preset);
|
||||
const merged = mergeMessages(presetMessages, messages);
|
||||
const generation = options.generation ?? preset.generation;
|
||||
|
||||
return this.inner.completeWithTools(merged, {
|
||||
const prepared = this.prepare(messages, options);
|
||||
return this.inner.completeWithTools(prepared.messages, {
|
||||
...options,
|
||||
generation,
|
||||
...prepared.options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,17 +105,14 @@ export class PresetLlmProvider implements LlmProvider {
|
||||
options: CompleteWithToolsOptions,
|
||||
callbacks: StreamCallbacks,
|
||||
): Promise<CompleteWithToolsResult> {
|
||||
const preset = this.getPreset();
|
||||
if (!this.inner.completeWithToolsStream) {
|
||||
return this.completeWithTools(messages, options);
|
||||
}
|
||||
const presetMessages = preset ? assemblePresetMessages(preset) : [];
|
||||
const merged = preset ? mergeMessages(presetMessages, messages) : messages;
|
||||
const generation = options.generation ?? preset?.generation;
|
||||
|
||||
return this.inner.completeWithToolsStream(merged, {
|
||||
...options,
|
||||
generation,
|
||||
}, callbacks);
|
||||
const prepared = this.prepare(messages, options);
|
||||
return this.inner.completeWithToolsStream(
|
||||
prepared.messages,
|
||||
{ ...options, ...prepared.options },
|
||||
callbacks,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
const storage = new AsyncLocalStorage<AbortSignal>();
|
||||
|
||||
export function runWithAbortSignal<T>(
|
||||
signal: AbortSignal,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
return storage.run(signal, fn);
|
||||
}
|
||||
|
||||
export function currentAbortSignal(): AbortSignal | undefined {
|
||||
return storage.getStore();
|
||||
}
|
||||
|
||||
export function isAbortError(err: unknown): boolean {
|
||||
if (!err || typeof err !== "object") return false;
|
||||
const name = "name" in err ? String(err.name) : "";
|
||||
if (name === "AbortError") return true;
|
||||
const message = "message" in err ? String(err.message) : "";
|
||||
return /aborted|AbortError/i.test(message);
|
||||
}
|
||||
|
||||
export function throwIfAborted(signal?: AbortSignal): void {
|
||||
const active = signal ?? currentAbortSignal();
|
||||
if (!active?.aborted) return;
|
||||
const err = new Error("This operation was aborted");
|
||||
err.name = "AbortError";
|
||||
throw err;
|
||||
}
|
||||
@@ -32,14 +32,21 @@ export type LlmTrackingContext = {
|
||||
|
||||
function capturePendingTrace(
|
||||
ctx: LlmTrackingContext,
|
||||
messages: Array<{ role: string; content: string }>,
|
||||
messages: Array<{
|
||||
role: string;
|
||||
content?: string | null;
|
||||
tool_calls?: unknown;
|
||||
tool_call_id?: string;
|
||||
}>,
|
||||
caller: string | undefined,
|
||||
model?: string,
|
||||
generation?: CompleteOptions["generation"],
|
||||
): void {
|
||||
ctx.pendingContextTrace = buildContextTrace({
|
||||
caller: caller ?? "unknown",
|
||||
messages,
|
||||
model,
|
||||
generation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,7 +137,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
||||
async () => {
|
||||
const result = await this.inner.complete(messages, options);
|
||||
const ctx = this.getContext();
|
||||
capturePendingTrace(ctx, messages, options?.caller, result.model);
|
||||
capturePendingTrace(
|
||||
ctx,
|
||||
messages,
|
||||
options?.caller,
|
||||
result.model,
|
||||
options?.generation,
|
||||
);
|
||||
recordUsage(ctx, options?.caller, result.model, result.usage);
|
||||
if (result.reasoning?.trim()) {
|
||||
ctx.pendingReasoning = result.reasoning.trim();
|
||||
@@ -168,7 +181,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
||||
onContentDelta: callbacks.onContentDelta,
|
||||
});
|
||||
const ctx = this.getContext();
|
||||
capturePendingTrace(ctx, messages, options?.caller, result.model);
|
||||
capturePendingTrace(
|
||||
ctx,
|
||||
messages,
|
||||
options?.caller,
|
||||
result.model,
|
||||
options?.generation,
|
||||
);
|
||||
recordUsage(ctx, options?.caller, result.model, result.usage);
|
||||
if (result.reasoning?.trim()) {
|
||||
ctx.pendingReasoning = result.reasoning.trim();
|
||||
@@ -189,7 +208,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
||||
async () => {
|
||||
const result = await this.inner.completeWithTools(messages, options);
|
||||
const ctx = this.getContext();
|
||||
capturePendingTrace(ctx, messages, options.caller, result.model);
|
||||
capturePendingTrace(
|
||||
ctx,
|
||||
messages,
|
||||
options.caller,
|
||||
result.model,
|
||||
options.generation,
|
||||
);
|
||||
recordUsage(ctx, options.caller, result.model, result.usage);
|
||||
if (result.reasoning?.trim()) {
|
||||
ctx.pendingReasoning = result.reasoning.trim();
|
||||
@@ -224,7 +249,13 @@ export class TokenTrackingProvider implements LlmProvider {
|
||||
onContentDelta: callbacks.onContentDelta,
|
||||
});
|
||||
const ctx = this.getContext();
|
||||
capturePendingTrace(ctx, messages, options.caller, result.model);
|
||||
capturePendingTrace(
|
||||
ctx,
|
||||
messages,
|
||||
options.caller,
|
||||
result.model,
|
||||
options.generation,
|
||||
);
|
||||
recordUsage(ctx, options.caller, result.model, result.usage);
|
||||
if (result.reasoning?.trim()) {
|
||||
ctx.pendingReasoning = result.reasoning.trim();
|
||||
|
||||
@@ -77,6 +77,9 @@ const EFFECT_LABEL: Record<string, string> = {
|
||||
run_programmatic_review: "程序验收",
|
||||
emit_message: "发消息",
|
||||
propose_next_creation_step: "提案下一步",
|
||||
seal_creation_opening: "收口开场白",
|
||||
run_play_turn: "开游玩回合",
|
||||
continue_play_turn: "续游玩管线",
|
||||
};
|
||||
|
||||
const TOOL_LABEL: Record<string, string> = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { LlmProvider } from "../llm/client.js";
|
||||
import { createLocalLlmDriver, type AgentDriver } from "../runtime/driver.js";
|
||||
import { normalizeQuestions } from "../skills/question-protocol.js";
|
||||
import type { BlackboardTagIndex } from "../types/blackboard.js";
|
||||
import type { MainAgentDecision, RuntimeSession } from "../types/runtime.js";
|
||||
@@ -45,7 +46,14 @@ ${workerLines}
|
||||
}
|
||||
|
||||
export class MainAgent {
|
||||
constructor(private readonly llm: LlmProvider) {}
|
||||
private readonly driver: AgentDriver;
|
||||
|
||||
constructor(
|
||||
private readonly llm: LlmProvider,
|
||||
driver?: AgentDriver,
|
||||
) {
|
||||
this.driver = driver ?? createLocalLlmDriver(llm);
|
||||
}
|
||||
|
||||
/** @deprecated 单次 JSON 决策;请用 runToolLoop */
|
||||
async decide(context: MainAgentContext): Promise<MainAgentDecision> {
|
||||
@@ -70,6 +78,7 @@ export class MainAgent {
|
||||
this.llm,
|
||||
context,
|
||||
options.handlers,
|
||||
this.driver,
|
||||
);
|
||||
return decision;
|
||||
}
|
||||
|
||||
+56
-111
@@ -1,12 +1,15 @@
|
||||
import type { ChatMessage, LlmProvider, ParsedToolCall, StreamCallbacks } from "../llm/client.js";
|
||||
import { supportsToolStream } from "../llm/stream-complete.js";
|
||||
import type { LlmProvider, ParsedToolCall } from "../llm/client.js";
|
||||
import { toolCallToDecision, validateLoopToolCall } from "../runtime/tool-registry.js";
|
||||
import {
|
||||
createLocalLlmDriver,
|
||||
type AgentDriver,
|
||||
type DriverToolCall,
|
||||
} from "../runtime/driver.js";
|
||||
import type { MainAgentDecision } from "../types/runtime.js";
|
||||
import { isMainAgentTerminalTool } from "../types/tools.js";
|
||||
import { MAIN_AGENT_TOOL_DEFINITIONS } from "./tools.js";
|
||||
import { buildMainAgentUserPrompt, parseMainAgentDecision } from "./main-agent.js";
|
||||
import type { MainAgentContext } from "./main-agent.js";
|
||||
import { debugLog } from "../log.js";
|
||||
|
||||
export type ToolLoopHandlers = {
|
||||
readBlackboard: (tags: string[]) => Record<string, string>;
|
||||
@@ -31,8 +34,6 @@ export type ToolLoopResult = {
|
||||
toolTrace: string[];
|
||||
};
|
||||
|
||||
const MAX_TOOL_LOOP_ITERATIONS = 12;
|
||||
|
||||
function buildToolLoopSystemPrompt(
|
||||
workers: Array<{ id: string; description: string }>,
|
||||
): string {
|
||||
@@ -54,35 +55,15 @@ ${workerLines}
|
||||
选定节点后立即调用 run_worker。`;
|
||||
}
|
||||
|
||||
async function completeToolsPreferStream(
|
||||
llm: LlmProvider,
|
||||
messages: ChatMessage[],
|
||||
callbacks: StreamCallbacks,
|
||||
): Promise<Awaited<ReturnType<LlmProvider["completeWithTools"]>>> {
|
||||
if (supportsToolStream(llm)) {
|
||||
return llm.completeWithToolsStream!(messages, {
|
||||
tools: MAIN_AGENT_TOOL_DEFINITIONS,
|
||||
caller: "main_agent",
|
||||
}, callbacks);
|
||||
}
|
||||
const result = await llm.completeWithTools(messages, {
|
||||
tools: MAIN_AGENT_TOOL_DEFINITIONS,
|
||||
caller: "main_agent",
|
||||
});
|
||||
if (result.reasoning) {
|
||||
callbacks.onReasoningDelta?.(result.reasoning);
|
||||
}
|
||||
if (result.content) {
|
||||
callbacks.onContentDelta?.(result.content);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function executeLoopTool(
|
||||
call: ParsedToolCall,
|
||||
call: DriverToolCall | ParsedToolCall,
|
||||
handlers: ToolLoopHandlers,
|
||||
): string {
|
||||
const name = validateLoopToolCall(call);
|
||||
const name = validateLoopToolCall({
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
arguments: call.arguments,
|
||||
});
|
||||
const args = JSON.parse(call.arguments || "{}") as Record<string, unknown>;
|
||||
|
||||
switch (name) {
|
||||
@@ -104,102 +85,66 @@ function executeLoopTool(
|
||||
}
|
||||
}
|
||||
|
||||
function assistantMessageFromToolCalls(
|
||||
content: string | null,
|
||||
toolCalls: ParsedToolCall[],
|
||||
): ChatMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
tool_calls: toolCalls.map((tc) => ({
|
||||
id: tc.id,
|
||||
type: "function" as const,
|
||||
function: { name: tc.name, arguments: tc.arguments },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function emitThinkingDone(handlers: ToolLoopHandlers, reasoning?: string): void {
|
||||
const text = reasoning?.trim() ?? "";
|
||||
if (text) handlers.onThinkingDone?.(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 总管 tool loop:在 running 相位内可多轮调用 read_blackboard 等,
|
||||
* 直到调用终止 tool 并返回 MainAgentDecision。
|
||||
*
|
||||
* 循环本身交给 {@link AgentDriver};本函数只解释总管 tool 语义。
|
||||
*/
|
||||
export async function runMainAgentToolLoop(
|
||||
llm: LlmProvider,
|
||||
context: MainAgentContext,
|
||||
handlers: ToolLoopHandlers,
|
||||
driver: AgentDriver = createLocalLlmDriver(llm),
|
||||
): Promise<ToolLoopResult> {
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: buildToolLoopSystemPrompt(context.availableWorkers),
|
||||
},
|
||||
{ role: "user", content: buildMainAgentUserPrompt(context) },
|
||||
];
|
||||
|
||||
const toolTrace: string[] = [];
|
||||
const streamCallbacks: StreamCallbacks = {
|
||||
onReasoningDelta: (delta) => handlers.onThinkingDelta?.(delta),
|
||||
};
|
||||
|
||||
for (let iteration = 1; iteration <= MAX_TOOL_LOOP_ITERATIONS; iteration++) {
|
||||
debugLog("llm", `总管循环 第${iteration}轮`);
|
||||
const result = await completeToolsPreferStream(llm, messages, streamCallbacks);
|
||||
emitThinkingDone(handlers, result.reasoning);
|
||||
const run = await driver.run({
|
||||
system: buildToolLoopSystemPrompt(context.availableWorkers),
|
||||
messages: [{ role: "user", content: buildMainAgentUserPrompt(context) }],
|
||||
tools: MAIN_AGENT_TOOL_DEFINITIONS,
|
||||
caller: "main_agent",
|
||||
label: "总管循环",
|
||||
onThinkingDelta: (delta) => handlers.onThinkingDelta?.(delta),
|
||||
onThinkingDone: (text) => handlers.onThinkingDone?.(text),
|
||||
handleStep: (calls) => {
|
||||
const terminalCalls = calls.filter((tc) => isMainAgentTerminalTool(tc.name));
|
||||
const loopCalls = calls.filter((tc) => !isMainAgentTerminalTool(tc.name));
|
||||
|
||||
if (result.toolCalls.length === 0) {
|
||||
if (result.content?.trim()) {
|
||||
const decision = parseMainAgentDecision(result.content);
|
||||
return { decision, iterations: iteration, toolTrace };
|
||||
if (terminalCalls.length > 1) {
|
||||
throw new Error(
|
||||
`Main Agent returned multiple terminal tools: ${terminalCalls.map((t) => t.name).join(", ")}`,
|
||||
);
|
||||
}
|
||||
throw new Error("Main Agent returned no tool calls and no content");
|
||||
}
|
||||
|
||||
const terminalCalls = result.toolCalls.filter((tc) =>
|
||||
isMainAgentTerminalTool(tc.name),
|
||||
);
|
||||
const loopCalls = result.toolCalls.filter(
|
||||
(tc) => !isMainAgentTerminalTool(tc.name),
|
||||
);
|
||||
if (terminalCalls.length === 1) {
|
||||
const terminal = terminalCalls[0];
|
||||
handlers.onToolCall?.(terminal.name, terminal.arguments);
|
||||
toolTrace.push(`${terminal.name} (terminal)`);
|
||||
return { kind: "conclude" as const, call: terminal };
|
||||
}
|
||||
|
||||
if (terminalCalls.length > 1) {
|
||||
throw new Error(
|
||||
`Main Agent returned multiple terminal tools: ${terminalCalls.map((t) => t.name).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
messages.push(
|
||||
assistantMessageFromToolCalls(result.content, result.toolCalls),
|
||||
);
|
||||
|
||||
if (terminalCalls.length === 1) {
|
||||
const terminal = terminalCalls[0];
|
||||
handlers.onToolCall?.(terminal.name, terminal.arguments);
|
||||
toolTrace.push(`${terminal.name} (terminal)`);
|
||||
return {
|
||||
decision: toolCallToDecision(terminal),
|
||||
iterations: iteration,
|
||||
toolTrace,
|
||||
};
|
||||
}
|
||||
|
||||
for (const call of loopCalls) {
|
||||
const output = executeLoopTool(call, handlers);
|
||||
handlers.onToolCall?.(call.name, output.slice(0, 200));
|
||||
toolTrace.push(call.name);
|
||||
messages.push({
|
||||
role: "tool",
|
||||
content: output,
|
||||
tool_call_id: call.id,
|
||||
const results = loopCalls.map((call) => {
|
||||
const output = executeLoopTool(call, handlers);
|
||||
handlers.onToolCall?.(call.name, output.slice(0, 200));
|
||||
toolTrace.push(call.name);
|
||||
return { callId: call.id, content: output };
|
||||
});
|
||||
}
|
||||
return { kind: "continue" as const, results };
|
||||
},
|
||||
});
|
||||
|
||||
if (run.stop.kind === "text") {
|
||||
return {
|
||||
decision: parseMainAgentDecision(run.stop.content),
|
||||
iterations: run.iterations,
|
||||
toolTrace,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Main Agent tool loop exceeded ${MAX_TOOL_LOOP_ITERATIONS} iterations`,
|
||||
);
|
||||
return {
|
||||
decision: toolCallToDecision(run.stop.call),
|
||||
iterations: run.iterations,
|
||||
toolTrace,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { ChatMessage } from "../llm/client.js";
|
||||
import type { PresetPackage } from "../types/preset.js";
|
||||
import { isStUnfilledMarker } from "./markers.js";
|
||||
|
||||
export type MarkerResolver = (identifier: string) => string | null;
|
||||
|
||||
const defaultMarkerResolver: MarkerResolver = () => null;
|
||||
|
||||
/**
|
||||
* 按 prompt_order 装配 preset 消息,插入在所有业务 prompt 之前。
|
||||
* 按 prompt_order 装配 preset 消息。
|
||||
* 酒馆角色卡 / WI marker 不走 resolver(空洞跳过);自有洞由 resolver 填。
|
||||
*/
|
||||
export function assemblePresetMessages(
|
||||
preset: PresetPackage,
|
||||
@@ -25,8 +27,10 @@ export function assemblePresetMessages(
|
||||
if (!entry || !entry.enabled) continue;
|
||||
|
||||
let content = entry.content.trim();
|
||||
if (!content && entry.marker) {
|
||||
const resolved = resolveMarker(entry.sourceIdentifier);
|
||||
const markerId = entry.sourceIdentifier || entry.id;
|
||||
const skipFill = isStUnfilledMarker(markerId);
|
||||
if (!content && entry.marker && !skipFill) {
|
||||
const resolved = resolveMarker(markerId);
|
||||
if (resolved?.trim()) content = resolved.trim();
|
||||
}
|
||||
|
||||
|
||||
+16
-16
@@ -7,6 +7,7 @@ import type {
|
||||
PresetPromptOrderItem,
|
||||
UnsupportedPresetSection,
|
||||
} from "../types/preset.js";
|
||||
import { applyExtendedMarkers } from "./markers.js";
|
||||
|
||||
type StPrompt = {
|
||||
identifier?: string;
|
||||
@@ -186,17 +187,6 @@ export function importSillyTavernPreset(
|
||||
});
|
||||
});
|
||||
|
||||
const referenced = new Set(promptOrder.map((o) => o.promptId));
|
||||
const unreferencedCount = prompts.filter(
|
||||
(p) => !referenced.has(p.id),
|
||||
).length;
|
||||
|
||||
const enabledCount = promptOrder.filter((o) => {
|
||||
if (!o.enabled) return false;
|
||||
const entry = prompts.find((p) => p.id === o.promptId);
|
||||
return entry?.enabled !== false;
|
||||
}).length;
|
||||
|
||||
const generation = readGeneration(raw);
|
||||
const generationFields = Object.entries(generation)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
@@ -211,7 +201,7 @@ export function importSillyTavernPreset(
|
||||
? (rawInput as { name: string }).name
|
||||
: "导入的预设");
|
||||
|
||||
const preset: PresetPackage = {
|
||||
const preset: PresetPackage = applyExtendedMarkers({
|
||||
id: options.id ?? `${slugify(presetName) || "preset"}-${randomUUID().slice(0, 8)}`,
|
||||
name: presetName,
|
||||
source: "sillytavern",
|
||||
@@ -221,13 +211,23 @@ export function importSillyTavernPreset(
|
||||
unsupported: collectUnsupported(raw),
|
||||
importedAt: new Date().toISOString(),
|
||||
raw: rawInput,
|
||||
};
|
||||
});
|
||||
|
||||
const framedReferenced = new Set(preset.promptOrder.map((o) => o.promptId));
|
||||
const framedUnreferenced = preset.prompts.filter(
|
||||
(p) => !framedReferenced.has(p.id),
|
||||
).length;
|
||||
const framedEnabled = preset.promptOrder.filter((o) => {
|
||||
if (!o.enabled) return false;
|
||||
const entry = preset.prompts.find((p) => p.id === o.promptId);
|
||||
return entry?.enabled !== false;
|
||||
}).length;
|
||||
|
||||
return {
|
||||
preset,
|
||||
promptCount: prompts.length,
|
||||
enabledCount,
|
||||
unreferencedCount,
|
||||
promptCount: preset.prompts.length,
|
||||
enabledCount: framedEnabled,
|
||||
unreferencedCount: framedUnreferenced,
|
||||
missingIdentifiers,
|
||||
generationFields,
|
||||
warnings,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import type {
|
||||
PresetPackage,
|
||||
PresetPromptEntry,
|
||||
PresetPromptOrderItem,
|
||||
PresetPromptRole,
|
||||
} from "../types/preset.js";
|
||||
|
||||
/** 酒馆角色卡 / WI marker:导入后保留位置,运行时不填我们的正文。 */
|
||||
export const ST_UNFILLED_MARKERS = new Set([
|
||||
"worldInfoBefore",
|
||||
"personaDescription",
|
||||
"charDescription",
|
||||
"charPersonality",
|
||||
"scenario",
|
||||
"worldInfoAfter",
|
||||
"dialogueExamples",
|
||||
]);
|
||||
|
||||
export const CHAT_HISTORY_MARKER = "chatHistory";
|
||||
export const WORLD_BOOK_BEFORE = "worldBookBefore";
|
||||
export const WORLD_BOOK_AFTER = "worldBookAfter";
|
||||
export const CURRENT_TURN = "currentTurn";
|
||||
export const POST_TURN = "postTurn";
|
||||
|
||||
/** 导入时插入的自有洞(不含共用的 chatHistory)。 */
|
||||
export const APP_INSERTED_MARKERS = [
|
||||
WORLD_BOOK_BEFORE,
|
||||
WORLD_BOOK_AFTER,
|
||||
CURRENT_TURN,
|
||||
POST_TURN,
|
||||
] as const;
|
||||
|
||||
const APP_INSERTED_SET = new Set<string>(APP_INSERTED_MARKERS);
|
||||
|
||||
type MarkerDef = {
|
||||
id: string;
|
||||
name: string;
|
||||
role: PresetPromptRole;
|
||||
};
|
||||
|
||||
const MARKER_DEFS: MarkerDef[] = [
|
||||
{ id: WORLD_BOOK_BEFORE, name: "世界书(历史前)", role: "system" },
|
||||
{ id: CHAT_HISTORY_MARKER, name: "对话历史", role: "system" },
|
||||
{ id: WORLD_BOOK_AFTER, name: "世界书(历史后)", role: "system" },
|
||||
{ id: CURRENT_TURN, name: "本轮对话", role: "user" },
|
||||
{ id: POST_TURN, name: "本轮之后", role: "system" },
|
||||
];
|
||||
|
||||
function upsertMarkerEntry(
|
||||
prompts: PresetPromptEntry[],
|
||||
def: MarkerDef,
|
||||
): PresetPromptEntry[] {
|
||||
if (prompts.some((p) => p.id === def.id)) return prompts;
|
||||
return [
|
||||
...prompts,
|
||||
{
|
||||
id: def.id,
|
||||
name: def.name,
|
||||
enabled: true,
|
||||
role: def.role,
|
||||
content: "",
|
||||
marker: true,
|
||||
sourceIdentifier: def.id,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function reindex(
|
||||
order: Omit<PresetPromptOrderItem, "orderIndex">[],
|
||||
): PresetPromptOrderItem[] {
|
||||
return order.map((item, orderIndex) => ({ ...item, orderIndex }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 chatHistory 为锚点插入四个自有 marker。
|
||||
* 可重复调用:先摘掉已插入的自有洞,再按规则放回。
|
||||
*/
|
||||
export function applyExtendedMarkers(preset: PresetPackage): PresetPackage {
|
||||
let prompts = [...preset.prompts];
|
||||
for (const def of MARKER_DEFS) {
|
||||
prompts = upsertMarkerEntry(prompts, def);
|
||||
}
|
||||
|
||||
const rawOrder = [...preset.promptOrder]
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
.filter((item) => !APP_INSERTED_SET.has(item.promptId));
|
||||
|
||||
const histIndex = rawOrder.findIndex(
|
||||
(item) => item.promptId === CHAT_HISTORY_MARKER,
|
||||
);
|
||||
|
||||
const inserted = (id: string): PresetPromptOrderItem => ({
|
||||
promptId: id,
|
||||
enabled: true,
|
||||
orderIndex: 0,
|
||||
});
|
||||
|
||||
let next: PresetPromptOrderItem[];
|
||||
if (histIndex < 0) {
|
||||
next = reindex([
|
||||
...rawOrder,
|
||||
inserted(WORLD_BOOK_BEFORE),
|
||||
inserted(CHAT_HISTORY_MARKER),
|
||||
inserted(WORLD_BOOK_AFTER),
|
||||
inserted(CURRENT_TURN),
|
||||
inserted(POST_TURN),
|
||||
]);
|
||||
} else {
|
||||
const before = rawOrder.slice(0, histIndex);
|
||||
const history = rawOrder[histIndex]!;
|
||||
const after = rawOrder.slice(histIndex + 1);
|
||||
next = reindex([
|
||||
...before,
|
||||
inserted(WORLD_BOOK_BEFORE),
|
||||
history,
|
||||
inserted(WORLD_BOOK_AFTER),
|
||||
inserted(CURRENT_TURN),
|
||||
...after,
|
||||
inserted(POST_TURN),
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
...preset,
|
||||
prompts,
|
||||
promptOrder: next,
|
||||
};
|
||||
}
|
||||
|
||||
export function isStUnfilledMarker(identifier: string): boolean {
|
||||
return ST_UNFILLED_MARKERS.has(identifier);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ChatMessage } from "../llm/client.js";
|
||||
import type { PresetPackage } from "../types/preset.js";
|
||||
import { assemblePresetMessages } from "./assembler.js";
|
||||
import { applyExtendedMarkers } from "./markers.js";
|
||||
import {
|
||||
resolveWorldInfoMarker,
|
||||
type WorldInfoPack,
|
||||
} from "./world-info-pack.js";
|
||||
|
||||
/**
|
||||
* Play worker:system 契约 + 按扩展 prompt_order 填洞。
|
||||
* 不要再把同一份 context-order 正文叠成第二包 user。
|
||||
*/
|
||||
export function assemblePlayWorkerMessages(params: {
|
||||
systemPrompt: string;
|
||||
preset: PresetPackage;
|
||||
pack: WorldInfoPack;
|
||||
}): ChatMessage[] {
|
||||
const framed = applyExtendedMarkers(params.preset);
|
||||
const filled = assemblePresetMessages(framed, (id) =>
|
||||
resolveWorldInfoMarker(params.pack, id),
|
||||
);
|
||||
return [{ role: "system", content: params.systemPrompt }, ...filled];
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { ChatMessage } from "../llm/client.js";
|
||||
import type { PresetPackage } from "../types/preset.js";
|
||||
import { assemblePlayWorkerMessages } from "./play-frame.js";
|
||||
import type { WorldInfoPack } from "./world-info-pack.js";
|
||||
|
||||
/** 试跑窗口用的短契约,不是 play worker SKILL。 */
|
||||
export const PROBE_SYSTEM_PROMPT =
|
||||
"试跑本条预设:根据已拼好的上下文写一轮短回复。产物是对白或叙述,对应本轮用户输入。";
|
||||
|
||||
export const DEFAULT_PROBE_CONTEXT = {
|
||||
loreBefore: "一座雨夜的港口旅馆。柜台点着油灯。",
|
||||
history: "店员:今晚只剩阁楼那间。\n你:好。",
|
||||
loreAfter: "时间:深夜。地点:旅馆大厅。",
|
||||
postTurn: "",
|
||||
};
|
||||
|
||||
export type PresetProbeInput = {
|
||||
message: string;
|
||||
loreBefore?: string;
|
||||
history?: string;
|
||||
loreAfter?: string;
|
||||
postTurn?: string;
|
||||
};
|
||||
|
||||
export type PresetProbeMessage = {
|
||||
role: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type PresetProbeResult = {
|
||||
messages: PresetProbeMessage[];
|
||||
reply: string | null;
|
||||
reasoning: string | null;
|
||||
completed: boolean;
|
||||
skipReason?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
function optionalEntry(
|
||||
id: string,
|
||||
name: string,
|
||||
content: string,
|
||||
): WorldInfoPack["worldBookBefore"] {
|
||||
const text = content.trim();
|
||||
return text ? [{ id, name, content: text }] : [];
|
||||
}
|
||||
|
||||
export function worldInfoPackFromProbeInput(
|
||||
input: PresetProbeInput,
|
||||
): WorldInfoPack {
|
||||
const loreBefore = input.loreBefore ?? DEFAULT_PROBE_CONTEXT.loreBefore;
|
||||
const history = input.history ?? DEFAULT_PROBE_CONTEXT.history;
|
||||
const loreAfter = input.loreAfter ?? DEFAULT_PROBE_CONTEXT.loreAfter;
|
||||
const postTurn = input.postTurn ?? DEFAULT_PROBE_CONTEXT.postTurn;
|
||||
|
||||
return {
|
||||
worldBookBefore: optionalEntry("probe-before", "试跑设定", loreBefore),
|
||||
history: history.trim(),
|
||||
worldBookAfter: optionalEntry("probe-after", "本轮状态", loreAfter),
|
||||
turn: input.message.trim(),
|
||||
postTurn: optionalEntry("probe-post", "尾注", postTurn),
|
||||
};
|
||||
}
|
||||
|
||||
export function assemblePresetProbe(
|
||||
preset: PresetPackage,
|
||||
input: PresetProbeInput,
|
||||
): ChatMessage[] {
|
||||
return assemblePlayWorkerMessages({
|
||||
systemPrompt: PROBE_SYSTEM_PROMPT,
|
||||
preset,
|
||||
pack: worldInfoPackFromProbeInput(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function serializeProbeMessages(
|
||||
messages: ChatMessage[],
|
||||
): PresetProbeMessage[] {
|
||||
return messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: typeof m.content === "string" ? m.content : String(m.content ?? ""),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function runPresetProbe(params: {
|
||||
preset: PresetPackage;
|
||||
input: PresetProbeInput;
|
||||
complete?: (
|
||||
messages: ChatMessage[],
|
||||
) => Promise<{ content: string; reasoning?: string }>;
|
||||
}): Promise<PresetProbeResult> {
|
||||
const message = params.input.message.trim();
|
||||
if (!message) {
|
||||
throw new Error("缺少本轮输入");
|
||||
}
|
||||
|
||||
const messages = assemblePresetProbe(params.preset, {
|
||||
...params.input,
|
||||
message,
|
||||
});
|
||||
const serialized = serializeProbeMessages(messages);
|
||||
|
||||
if (!params.complete) {
|
||||
return {
|
||||
messages: serialized,
|
||||
reply: null,
|
||||
reasoning: null,
|
||||
completed: false,
|
||||
skipReason: "未请求生成",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await params.complete(messages);
|
||||
return {
|
||||
messages: serialized,
|
||||
reply: result.content,
|
||||
reasoning: result.reasoning ?? null,
|
||||
completed: true,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
messages: serialized,
|
||||
reply: null,
|
||||
reasoning: null,
|
||||
completed: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||
import type { BlackboardInputMerge } from "../types/blackboard.js";
|
||||
import {
|
||||
isDialogueHistoryRef,
|
||||
DIALOGUE_HISTORY_TAG,
|
||||
} from "../skills/dialogue-history.js";
|
||||
import {
|
||||
renderContextSegmentBody,
|
||||
type ContextSegmentDef,
|
||||
} from "../skills/context-segments.js";
|
||||
import {
|
||||
CHAT_HISTORY_MARKER,
|
||||
CURRENT_TURN,
|
||||
POST_TURN,
|
||||
WORLD_BOOK_AFTER,
|
||||
WORLD_BOOK_BEFORE,
|
||||
isStUnfilledMarker,
|
||||
} from "./markers.js";
|
||||
|
||||
export const CURRENT_TURN_TAG = "用户.最新输入";
|
||||
|
||||
const CURRENT_TURN_ALIASES = new Set([
|
||||
CURRENT_TURN_TAG,
|
||||
"currentTurn",
|
||||
"user.latest",
|
||||
"用户最新输入",
|
||||
]);
|
||||
|
||||
export function isCurrentTurnRef(ref: string): boolean {
|
||||
return CURRENT_TURN_ALIASES.has(ref.trim());
|
||||
}
|
||||
|
||||
export type WorldInfoEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type WorldInfoPack = {
|
||||
worldBookBefore: WorldInfoEntry[];
|
||||
history: string;
|
||||
worldBookAfter: WorldInfoEntry[];
|
||||
turn: string;
|
||||
postTurn: WorldInfoEntry[];
|
||||
};
|
||||
|
||||
function emptyPack(): WorldInfoPack {
|
||||
return {
|
||||
worldBookBefore: [],
|
||||
history: "",
|
||||
worldBookAfter: [],
|
||||
turn: "",
|
||||
postTurn: [],
|
||||
};
|
||||
}
|
||||
|
||||
function entryName(segment: ContextSegmentDef): string {
|
||||
const raw = (segment.label ?? "").replace(/^#+\s*/, "").trim();
|
||||
return raw || segment.id;
|
||||
}
|
||||
|
||||
function toEntry(segment: ContextSegmentDef, content: string): WorldInfoEntry {
|
||||
return {
|
||||
id: segment.id,
|
||||
name: entryName(segment),
|
||||
content: content.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatWorldInfoEntries(entries: WorldInfoEntry[]): string {
|
||||
return entries
|
||||
.filter((e) => e.content.trim())
|
||||
.map((e) => {
|
||||
const title = e.name.trim();
|
||||
const body = e.content.trim();
|
||||
return title ? `### ${title}\n\n${body}` : body;
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 contextSegments 相对「对话.历史」「用户.最新输入」切成四袋。
|
||||
*/
|
||||
export function worldInfoPackFromSegments(params: {
|
||||
segments: ContextSegmentDef[];
|
||||
inputs: Record<string, string>;
|
||||
blackboard: Blackboard;
|
||||
inputMerge?: BlackboardInputMerge;
|
||||
}): WorldInfoPack {
|
||||
const inputMerge = params.inputMerge ?? "latest";
|
||||
const pack = emptyPack();
|
||||
let phase: "before" | "after" | "post" = "before";
|
||||
|
||||
for (const segment of params.segments) {
|
||||
const tags = segment.tags ?? [];
|
||||
const isHist = tags.some((t) => isDialogueHistoryRef(t));
|
||||
const isTurn = tags.some((t) => isCurrentTurnRef(t));
|
||||
const body = renderContextSegmentBody(
|
||||
segment,
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
|
||||
if (isHist) {
|
||||
pack.history = body;
|
||||
phase = "after";
|
||||
continue;
|
||||
}
|
||||
if (isTurn) {
|
||||
pack.turn = body;
|
||||
phase = "post";
|
||||
continue;
|
||||
}
|
||||
if (!body) continue;
|
||||
const entry = toEntry(segment, body);
|
||||
if (phase === "before") pack.worldBookBefore.push(entry);
|
||||
else if (phase === "after") pack.worldBookAfter.push(entry);
|
||||
else pack.postTurn.push(entry);
|
||||
}
|
||||
|
||||
if (!pack.history.trim() && params.inputs[DIALOGUE_HISTORY_TAG]?.trim()) {
|
||||
pack.history = params.inputs[DIALOGUE_HISTORY_TAG]!.trim();
|
||||
}
|
||||
if (!pack.turn.trim() && params.inputs[CURRENT_TURN_TAG]?.trim()) {
|
||||
pack.turn = params.inputs[CURRENT_TURN_TAG]!.trim();
|
||||
}
|
||||
|
||||
return pack;
|
||||
}
|
||||
|
||||
export function resolveWorldInfoMarker(
|
||||
pack: WorldInfoPack,
|
||||
identifier: string,
|
||||
): string | null {
|
||||
if (isStUnfilledMarker(identifier)) return null;
|
||||
switch (identifier) {
|
||||
case WORLD_BOOK_BEFORE: {
|
||||
const text = formatWorldInfoEntries(pack.worldBookBefore);
|
||||
return text || null;
|
||||
}
|
||||
case CHAT_HISTORY_MARKER:
|
||||
return pack.history.trim() || null;
|
||||
case WORLD_BOOK_AFTER: {
|
||||
const text = formatWorldInfoEntries(pack.worldBookAfter);
|
||||
return text || null;
|
||||
}
|
||||
case CURRENT_TURN:
|
||||
return pack.turn.trim() || null;
|
||||
case POST_TURN: {
|
||||
const text = formatWorldInfoEntries(pack.postTurn);
|
||||
return text || null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 可替换的 agent 司机:问模型 → 有 tool-call 则交给调用方处理 → 再问,直到不再欠请求。
|
||||
*
|
||||
* 相位机、黑板、规格不走这里。换 dsh / 其它 harness 时只换 {@link AgentDriver} 实现。
|
||||
*
|
||||
* @module runtime/driver
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChatMessage,
|
||||
CompleteWithToolsResult,
|
||||
LlmProvider,
|
||||
StreamCallbacks,
|
||||
ToolDefinition,
|
||||
} from "../llm/client.js";
|
||||
import { supportsToolStream } from "../llm/stream-complete.js";
|
||||
import { debugLog } from "../log.js";
|
||||
|
||||
export type DriverToolCall = {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
|
||||
/** 一步里所有 tool-call 的处理结果:继续把结果写回,或本步结账停循环。 */
|
||||
export type DriverStepOutcome =
|
||||
| { kind: "continue"; results: Array<{ callId: string; content: string }> }
|
||||
| { kind: "conclude"; call: DriverToolCall };
|
||||
|
||||
export type DriverRunInput = {
|
||||
system: string;
|
||||
messages: ChatMessage[];
|
||||
tools: ToolDefinition[];
|
||||
caller?: string;
|
||||
/** 日志用短名,如「总管循环」 */
|
||||
label?: string;
|
||||
maxIterations?: number;
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
onThinkingDone?: (text: string) => void;
|
||||
handleStep: (
|
||||
calls: DriverToolCall[],
|
||||
) => DriverStepOutcome | Promise<DriverStepOutcome>;
|
||||
};
|
||||
|
||||
export type DriverStop =
|
||||
| { kind: "text"; content: string }
|
||||
| { kind: "conclude"; call: DriverToolCall };
|
||||
|
||||
export type DriverRunResult = {
|
||||
iterations: number;
|
||||
stop: DriverStop;
|
||||
};
|
||||
|
||||
/** 司机:直到模型不再欠一次请求。 */
|
||||
export type AgentDriver = {
|
||||
run(input: DriverRunInput): Promise<DriverRunResult>;
|
||||
};
|
||||
|
||||
export const DEFAULT_DRIVER_MAX_ITERATIONS = 12;
|
||||
|
||||
function assistantMessageFromToolCalls(
|
||||
content: string | null,
|
||||
toolCalls: DriverToolCall[],
|
||||
): ChatMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
tool_calls: toolCalls.map((tc) => ({
|
||||
id: tc.id,
|
||||
type: "function" as const,
|
||||
function: { name: tc.name, arguments: tc.arguments },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function emitThinkingDone(
|
||||
input: DriverRunInput,
|
||||
reasoning?: string,
|
||||
): void {
|
||||
const text = reasoning?.trim() ?? "";
|
||||
if (text) input.onThinkingDone?.(text);
|
||||
}
|
||||
|
||||
async function completeToolsPreferStream(
|
||||
llm: LlmProvider,
|
||||
messages: ChatMessage[],
|
||||
tools: ToolDefinition[],
|
||||
caller: string | undefined,
|
||||
callbacks: StreamCallbacks,
|
||||
): Promise<CompleteWithToolsResult> {
|
||||
const options = { tools, caller };
|
||||
if (supportsToolStream(llm)) {
|
||||
return llm.completeWithToolsStream!(messages, options, callbacks);
|
||||
}
|
||||
const result = await llm.completeWithTools(messages, options);
|
||||
if (result.reasoning) callbacks.onReasoningDelta?.(result.reasoning);
|
||||
if (result.content) callbacks.onContentDelta?.(result.content);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 现有 OpenAI 兼容 provider 上的司机,行为对齐旧 tool-loop。 */
|
||||
export function createLocalLlmDriver(llm: LlmProvider): AgentDriver {
|
||||
return {
|
||||
async run(input: DriverRunInput): Promise<DriverRunResult> {
|
||||
const maxIterations = input.maxIterations ?? DEFAULT_DRIVER_MAX_ITERATIONS;
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: input.system },
|
||||
...input.messages,
|
||||
];
|
||||
const streamCallbacks: StreamCallbacks = {
|
||||
onReasoningDelta: (delta) => input.onThinkingDelta?.(delta),
|
||||
};
|
||||
const label = input.label ?? "driver";
|
||||
|
||||
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
||||
debugLog("llm", `${label} 第${iteration}轮`);
|
||||
const result = await completeToolsPreferStream(
|
||||
llm,
|
||||
messages,
|
||||
input.tools,
|
||||
input.caller,
|
||||
streamCallbacks,
|
||||
);
|
||||
emitThinkingDone(input, result.reasoning);
|
||||
|
||||
if (result.toolCalls.length === 0) {
|
||||
const content = result.content?.trim() ?? "";
|
||||
if (content) {
|
||||
return { iterations: iteration, stop: { kind: "text", content } };
|
||||
}
|
||||
throw new Error("Driver returned no tool calls and no content");
|
||||
}
|
||||
|
||||
const calls: DriverToolCall[] = result.toolCalls.map((tc) => ({
|
||||
id: tc.id,
|
||||
name: tc.name,
|
||||
arguments: tc.arguments,
|
||||
}));
|
||||
messages.push(assistantMessageFromToolCalls(result.content, calls));
|
||||
|
||||
const outcome = await input.handleStep(calls);
|
||||
if (outcome.kind === "conclude") {
|
||||
return {
|
||||
iterations: iteration,
|
||||
stop: { kind: "conclude", call: outcome.call },
|
||||
};
|
||||
}
|
||||
|
||||
for (const item of outcome.results) {
|
||||
messages.push({
|
||||
role: "tool",
|
||||
content: item.content,
|
||||
tool_call_id: item.callId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Driver exceeded ${maxIterations} iterations`);
|
||||
},
|
||||
};
|
||||
}
|
||||
+11
-19
@@ -1,23 +1,21 @@
|
||||
import { profileToLlmConfig } from "../config/api-profiles.js";
|
||||
import {
|
||||
ensureActiveProfileDefault,
|
||||
loadAppSettings,
|
||||
resolveActiveProfile,
|
||||
} from "../config/settings.js";
|
||||
import { resolveActivePreset } from "../preset/store.js";
|
||||
import {
|
||||
createMockMainAgentResponse,
|
||||
MockLlmProvider,
|
||||
OpenAiCompatibleProvider,
|
||||
type LlmProvider,
|
||||
} from "../llm/client.js";
|
||||
import { PresetLlmProvider } from "../llm/preset-wrapper.js";
|
||||
import type { PresetPackage } from "../types/preset.js";
|
||||
import {
|
||||
TokenTrackingProvider,
|
||||
type LlmTrackingContext,
|
||||
} from "../llm/token-tracker.js";
|
||||
wrapLlmForSession,
|
||||
type LlmTrackingRef,
|
||||
} from "../llm/preset-wrapper.js";
|
||||
|
||||
export type LlmTrackingRef = { current: LlmTrackingContext };
|
||||
export type { LlmTrackingRef };
|
||||
|
||||
function buildInnerLlm(): LlmProvider {
|
||||
ensureActiveProfileDefault();
|
||||
@@ -42,22 +40,11 @@ function buildInnerLlm(): LlmProvider {
|
||||
]);
|
||||
}
|
||||
|
||||
function wrapWithPreset(inner: LlmProvider): LlmProvider {
|
||||
const settings = loadAppSettings();
|
||||
const preset = resolveActivePreset(settings.activePresetId);
|
||||
if (!preset) return inner;
|
||||
return new PresetLlmProvider(inner, () =>
|
||||
resolveActivePreset(loadAppSettings().activePresetId),
|
||||
);
|
||||
}
|
||||
|
||||
/** Web/CLI 默认 LLM:本地 profile + 全局 preset + token 统计 */
|
||||
export function createDefaultMainAgentLlm(
|
||||
trackingRef?: LlmTrackingRef,
|
||||
): LlmProvider {
|
||||
const llm = wrapWithPreset(buildInnerLlm());
|
||||
if (!trackingRef) return llm;
|
||||
return new TokenTrackingProvider(llm, () => trackingRef.current);
|
||||
return wrapLlmForSession(buildInnerLlm(), trackingRef);
|
||||
}
|
||||
|
||||
export function hasRealLlmConfig(): boolean {
|
||||
@@ -76,3 +63,8 @@ export function reloadDefaultMainAgentLlm(
|
||||
): LlmProvider {
|
||||
return createDefaultMainAgentLlm(trackingRef);
|
||||
}
|
||||
|
||||
/** 试跑指定预设:生成参数跟这条预设走,不依赖当前选用。 */
|
||||
export function createLlmForPreset(preset: PresetPackage): LlmProvider {
|
||||
return wrapLlmForSession(buildInnerLlm(), undefined, () => preset);
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ export class RuntimeOrchestrator {
|
||||
await this.runProgrammaticReview(effect.artifactId);
|
||||
break;
|
||||
case "invoke_main_agent":
|
||||
case "propose_next_creation_step":
|
||||
case "seal_creation_opening":
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+138
-34
@@ -30,6 +30,12 @@ import {
|
||||
isModuleOpeningQuestions,
|
||||
normalizeQuestions,
|
||||
} from "../skills/question-protocol.js";
|
||||
import {
|
||||
CREATION_SEALED_WAITING_MESSAGE,
|
||||
SLOT_CREATION_SEALED_BY_OPENING,
|
||||
isOpeningSealArtifact,
|
||||
} from "../skills/opening-seal.js";
|
||||
import { inferLifecycleStage } from "../skills/worker-declaration.js";
|
||||
import type { QuestionItem } from "../types/questions.js";
|
||||
|
||||
function nowIso(): string {
|
||||
@@ -81,7 +87,45 @@ function mergeSlotText(
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return;
|
||||
const prev = String(slots[key] ?? "").trim();
|
||||
slots[key] = prev ? `${prev}\n\n${trimmed}` : trimmed;
|
||||
if (!prev) {
|
||||
slots[key] = trimmed;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
prev === trimmed ||
|
||||
prev.includes(`\n\n${trimmed}`) ||
|
||||
prev.startsWith(`${trimmed}\n\n`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
slots[key] = `${prev}\n\n${trimmed}`;
|
||||
}
|
||||
|
||||
/** 同一步内多次「按意见修改」叠意见;较新的写在后面 */
|
||||
function accumulateRevisionInstruction(prev: unknown, next: string): string {
|
||||
const trimmed = next.trim();
|
||||
const old = typeof prev === "string" ? prev.trim() : "";
|
||||
if (!old || old === trimmed) return trimmed;
|
||||
if (old.includes(trimmed)) return old;
|
||||
return `${old}\n\n——\n\n${trimmed}`;
|
||||
}
|
||||
|
||||
/** 验收后把本步意见并入「用户.需求」按时间保留,再清掉本步槽位以免下一步误进修订 */
|
||||
function archiveStepNotesIntoDemand(slots: Record<string, unknown>): void {
|
||||
const reply =
|
||||
typeof slots["用户.worker答复"] === "string"
|
||||
? slots["用户.worker答复"].trim()
|
||||
: "";
|
||||
const note =
|
||||
typeof slots["用户.修订说明"] === "string"
|
||||
? slots["用户.修订说明"].trim()
|
||||
: "";
|
||||
if (reply) mergeSlotText(slots, "用户.需求", reply);
|
||||
if (note) mergeSlotText(slots, "用户.需求", note);
|
||||
delete slots["用户.修订说明"];
|
||||
delete slots.revisionInstruction;
|
||||
delete slots.revisionTargetArtifactId;
|
||||
delete slots["用户.worker答复"];
|
||||
}
|
||||
|
||||
/** 追加事件到 history 并 touch */
|
||||
@@ -102,6 +146,30 @@ function running(session: RuntimeSession): RuntimeSession {
|
||||
return touch({ ...session, phase: "running", waitingReason: undefined });
|
||||
}
|
||||
|
||||
/** 游玩连跑下一执行单元;创作则交总管 */
|
||||
function afterPlayOrMainAgent(
|
||||
session: RuntimeSession,
|
||||
next: RuntimeSession,
|
||||
): ApplyEventResult {
|
||||
const cleared = {
|
||||
...next,
|
||||
pendingArtifactId: undefined,
|
||||
currentWorkerId: undefined,
|
||||
pendingDecision: undefined,
|
||||
resumeContext: undefined,
|
||||
};
|
||||
if (inferLifecycleStage(session) === "play") {
|
||||
return {
|
||||
session: touch(running(cleared)),
|
||||
effects: [{ type: "continue_play_turn" }],
|
||||
};
|
||||
}
|
||||
return {
|
||||
session: touch(running(cleared)),
|
||||
effects: [{ type: "invoke_main_agent" }],
|
||||
};
|
||||
}
|
||||
|
||||
function findArtifact(
|
||||
session: RuntimeSession,
|
||||
artifactId: string,
|
||||
@@ -150,11 +218,14 @@ function beginRevisionRerun(
|
||||
artifact: ArtifactRecord,
|
||||
instruction: string,
|
||||
): ApplyEventResult {
|
||||
const trimmed = instruction.trim();
|
||||
const trimmed = accumulateRevisionInstruction(
|
||||
session.slots["用户.修订说明"] ?? session.slots.revisionInstruction,
|
||||
instruction,
|
||||
);
|
||||
const slots: Record<string, unknown> = {
|
||||
...session.slots,
|
||||
"用户.修订说明": trimmed,
|
||||
"用户.最新输入": trimmed,
|
||||
"用户.最新输入": instruction.trim(),
|
||||
revisionInstruction: trimmed,
|
||||
// 本次重跑的底稿;重跑写不出新版时只许退回这一份
|
||||
revisionTargetArtifactId: artifact.id,
|
||||
@@ -319,7 +390,10 @@ function handleWorkerCompleted(
|
||||
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
||||
}
|
||||
|
||||
const mode = session.acceptanceMode ?? "user_confirmed";
|
||||
const mode =
|
||||
inferLifecycleStage(session) === "play"
|
||||
? "no_confirmation"
|
||||
: (session.acceptanceMode ?? "user_confirmed");
|
||||
let next = appendHistory(session, event);
|
||||
|
||||
if (next.slots.revisionTargetArtifactId) {
|
||||
@@ -346,16 +420,13 @@ function handleWorkerCompleted(
|
||||
|
||||
if (mode === "no_confirmation") {
|
||||
next = updateArtifact(next, artifact.id, { status: "accepted" });
|
||||
return {
|
||||
session: touch({
|
||||
...running(next),
|
||||
pendingArtifactId: undefined,
|
||||
currentWorkerId: undefined,
|
||||
pendingDecision: undefined,
|
||||
resumeContext: undefined,
|
||||
}),
|
||||
effects: [{ type: "invoke_main_agent" }],
|
||||
};
|
||||
return afterPlayOrMainAgent(session, {
|
||||
...next,
|
||||
pendingArtifactId: undefined,
|
||||
currentWorkerId: undefined,
|
||||
pendingDecision: undefined,
|
||||
resumeContext: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// programmatic_review
|
||||
@@ -587,9 +658,8 @@ export function applyEvent(
|
||||
if (session.waitingReason?.kind === "worker_questions") {
|
||||
slots["用户.worker答复"] = text;
|
||||
if (text) slots["用户.最新输入"] = text;
|
||||
if (demandKey && text) {
|
||||
mergeSlotText(slots, demandKey, text);
|
||||
}
|
||||
// 开场/追问答复只进「用户.worker答复」,不追加进「用户.需求」
|
||||
// (否则后续步骤的用户表述会把各步答复叠成一份长转录,修订意见被淹没)
|
||||
} else if (session.waitingReason?.kind === "input" && text) {
|
||||
const key = demandKey || "用户.需求";
|
||||
applyUserTextToSlots(slots, text, key, {
|
||||
@@ -615,6 +685,28 @@ export function applyEvent(
|
||||
};
|
||||
}
|
||||
|
||||
if (inferLifecycleStage(session) === "play") {
|
||||
return {
|
||||
session: touch(running({ ...next, resumeContext: undefined })),
|
||||
effects: [{ type: "run_play_turn" }],
|
||||
};
|
||||
}
|
||||
|
||||
if (session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
|
||||
return {
|
||||
session: waiting(touch({ ...next, resumeContext: undefined }), {
|
||||
kind: "input",
|
||||
message: CREATION_SEALED_WAITING_MESSAGE,
|
||||
}),
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message: "创作已收口并保存。请切换到「游玩」开始。",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 常规定稿用户输入 → 等总管下一步
|
||||
return {
|
||||
session: touch(running({ ...next, resumeContext: undefined })),
|
||||
@@ -817,7 +909,10 @@ export function applyEvent(
|
||||
|
||||
case "worker_completed": {
|
||||
const result = handleWorkerCompleted(session, event);
|
||||
const mode = session.acceptanceMode ?? "user_confirmed";
|
||||
const mode =
|
||||
inferLifecycleStage(session) === "play"
|
||||
? "no_confirmation"
|
||||
: (session.acceptanceMode ?? "user_confirmed");
|
||||
if (mode === "user_confirmed" && result.session.pendingArtifactId) {
|
||||
const sidecar = asOptionalSidecarQuestions(event.payload.questions);
|
||||
const assessment =
|
||||
@@ -908,6 +1003,7 @@ export function applyEvent(
|
||||
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
||||
}
|
||||
const slots: Record<string, unknown> = { ...session.slots };
|
||||
archiveStepNotesIntoDemand(slots);
|
||||
// 仅终稿 tag「设计.worker集」表示完整 Worker 集验收;草稿单位验收不得开 play
|
||||
const isFinalSet = artifact.outputTags.some((tag) => tag === "设计.worker集");
|
||||
if (isFinalSet) {
|
||||
@@ -924,24 +1020,29 @@ export function applyEvent(
|
||||
pendingDecision: undefined,
|
||||
currentWorkerId: undefined,
|
||||
};
|
||||
// 创作步验收后:先问下一步意向,再展示下一节点确认开干(勿直接连跑)
|
||||
const gateNextIntent =
|
||||
!isFinalSet &&
|
||||
(artifact.workerId === "design-step" ||
|
||||
artifact.workerId === "design-flow");
|
||||
if (gateNextIntent) {
|
||||
// 开场白终节点:选定后落库、关 DAG,不再问「下一步想写什么」。
|
||||
if (isOpeningSealArtifact(artifact)) {
|
||||
return {
|
||||
session: waiting(cleared, {
|
||||
kind: "next_intent",
|
||||
afterWorkerId: artifact.workerId,
|
||||
kind: "input",
|
||||
message: CREATION_SEALED_WAITING_MESSAGE,
|
||||
}),
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message:
|
||||
"这一步已收下。下一步想写什么?可留空,直接发送后看下一节点并确认开干。",
|
||||
},
|
||||
],
|
||||
effects: [{ type: "seal_creation_opening" }],
|
||||
};
|
||||
}
|
||||
if (inferLifecycleStage(session) === "play") {
|
||||
return afterPlayOrMainAgent(session, cleared);
|
||||
}
|
||||
// 创作步验收后直接提案下一步(有待执行步则确认开干;否则交总管扩步)。
|
||||
// 不再先停在「下一步想写什么」——与「同意并开始」叠成双重确认。
|
||||
// 细化终稿也走提案,而不是 invoke_main_agent(否则容易把刚收下的终稿再跑一遍)。
|
||||
const proposeNext =
|
||||
artifact.workerId === "design-step" ||
|
||||
artifact.workerId === "design-flow";
|
||||
if (proposeNext) {
|
||||
return {
|
||||
session: touch(running(cleared)),
|
||||
effects: [{ type: "propose_next_creation_step" }],
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -1029,7 +1130,10 @@ export function applyEvent(
|
||||
session: touch(
|
||||
running({ ...next, pendingArtifactId: undefined }),
|
||||
),
|
||||
effects: [{ type: "invoke_main_agent" }],
|
||||
effects:
|
||||
inferLifecycleStage(session) === "play"
|
||||
? [{ type: "continue_play_turn" }]
|
||||
: [{ type: "invoke_main_agent" }],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+239
-26
@@ -62,6 +62,7 @@ import {
|
||||
CREATION_MODULE_OPENING_STATE_TAG,
|
||||
SLOT_CREATION_MODULE_OPENING_STATE,
|
||||
loadModuleCatalog,
|
||||
isReviseStep,
|
||||
mergeCreationFlowPreservingAccepted,
|
||||
nextPendingStep,
|
||||
parseCreationFlow,
|
||||
@@ -71,10 +72,12 @@ import {
|
||||
resolveDesignStepBinding,
|
||||
shouldSkipModuleOpening,
|
||||
isProgressPointerTag,
|
||||
missingRequiredStepParams,
|
||||
stepUnitId,
|
||||
stringifyCreationFlow,
|
||||
stringifyModuleOpeningState,
|
||||
type CreationFlowStepParams,
|
||||
type ModuleCatalogEntry,
|
||||
type ModuleParamSpec,
|
||||
} from "../skills/creation-flow.js";
|
||||
import {
|
||||
@@ -86,6 +89,22 @@ import {
|
||||
SLOT_ASKED_QUESTIONS,
|
||||
} from "../skills/question-protocol.js";
|
||||
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
||||
import {
|
||||
closeCreationFlowRaw,
|
||||
CREATION_SEALED_WAITING_MESSAGE,
|
||||
mergeOpeningTablePatch,
|
||||
OPENING_CURRENT_VARS_TAG,
|
||||
OPENING_INITIAL_VARS_TAG,
|
||||
OPENING_OUTPUT_TAG,
|
||||
OPENING_SETUP_ARTIFACT_TAG,
|
||||
parseOpeningSealPayload,
|
||||
SLOT_CREATION_SEALED_BY_OPENING,
|
||||
SLOT_OPENING_SELECTED_INDEX,
|
||||
} from "../skills/opening-seal.js";
|
||||
import {
|
||||
readPlayTurnQueue,
|
||||
withPlayTurnQueue,
|
||||
} from "../skills/play-turn.js";
|
||||
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
||||
import {
|
||||
executeChance,
|
||||
@@ -272,6 +291,26 @@ export class PhaseRuntime {
|
||||
this.lastWorkerRunSnapshot = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行中卡住:有进行中的 worker 则按开跑快照重跑;否则重跑总管。
|
||||
*/
|
||||
async retryStuckRun(): Promise<RuntimeSession> {
|
||||
const workerId = this.session.currentWorkerId;
|
||||
const snap = this.lastWorkerRunSnapshot;
|
||||
if (workerId && snap && snap.workerId === workerId) {
|
||||
this.restoreFromCheckpoint(
|
||||
snap.runtimeSession,
|
||||
snap.blackboardItems,
|
||||
);
|
||||
return this.rerunWorker(workerId);
|
||||
}
|
||||
if (this.session.phase === "running" && !this.session.waitingReason) {
|
||||
await this.runMainAgent();
|
||||
return this.session;
|
||||
}
|
||||
throw new Error("当前没有可重试的执行");
|
||||
}
|
||||
|
||||
/** 重 roll 指定 worker(刷新 Skill 回复) */
|
||||
async rerunWorker(workerId: string): Promise<RuntimeSession> {
|
||||
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
||||
@@ -575,15 +614,38 @@ export class PhaseRuntime {
|
||||
}
|
||||
|
||||
/** 用户接受 pendingArtifact;创作单位验收 vs 终稿验收分流 */
|
||||
async acceptArtifact(artifactId?: string): Promise<RuntimeSession> {
|
||||
async acceptArtifact(
|
||||
artifactId?: string,
|
||||
opts?: { openingIndex?: number },
|
||||
): Promise<RuntimeSession> {
|
||||
const id = artifactId ?? this.session.pendingArtifactId;
|
||||
if (!id) throw new Error("当前没有待验收的产物");
|
||||
const artifact = this.session.artifacts.find((a) => a.id === id);
|
||||
if (artifact && opts?.openingIndex != null && Number.isFinite(opts.openingIndex)) {
|
||||
this.session = {
|
||||
...this.session,
|
||||
slots: {
|
||||
...this.session.slots,
|
||||
[SLOT_OPENING_SELECTED_INDEX]: Math.trunc(opts.openingIndex),
|
||||
},
|
||||
};
|
||||
}
|
||||
const unitAccept = artifact ? isDesignUnitArtifact(artifact) : false;
|
||||
// 细化终稿写「设计.worker集」,不是单位草稿,但仍须记入已验收步骤,
|
||||
// 否则编排器会当成没做完再跑一遍。须在 dispatch 之前写入。
|
||||
const shouldRecordStep = Boolean(
|
||||
artifact && (unitAccept || artifact.workerId === "design-step"),
|
||||
);
|
||||
if (artifact && shouldRecordStep) {
|
||||
const slots = { ...this.session.slots };
|
||||
this.recordCreationUnitAccepted(slots, artifact);
|
||||
this.session = { ...this.session, slots };
|
||||
}
|
||||
|
||||
await this.dispatch({ type: "user_accepted_artifact", payload: { artifactId: id } });
|
||||
this.persistInstanceWorkerDeclaration();
|
||||
|
||||
if (artifact) {
|
||||
const unitAccept = isDesignUnitArtifact(artifact);
|
||||
const result = compressAfterWorkerAccept({
|
||||
blackboard: this.blackboard,
|
||||
workerId: artifact.workerId,
|
||||
@@ -598,8 +660,7 @@ export class PhaseRuntime {
|
||||
lastCompressedWorkerId: artifact.workerId,
|
||||
};
|
||||
|
||||
if (unitAccept) {
|
||||
this.recordCreationUnitAccepted(slots, artifact);
|
||||
if (shouldRecordStep) {
|
||||
const unitId = String(slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
|
||||
this.onMessage(
|
||||
`[创作单位已验收] ${unitId || artifact.workerId}:${
|
||||
@@ -608,7 +669,8 @@ export class PhaseRuntime {
|
||||
);
|
||||
slots[SLOT_CREATION_CURRENT_UNIT] = undefined;
|
||||
slots[SLOT_CREATION_UNIT_ANCHOR_AT] = undefined;
|
||||
} else {
|
||||
}
|
||||
if (!unitAccept) {
|
||||
this.onMessage(
|
||||
`[上下文已压缩] ${artifact.workerId}:保留 ${result.finals.map((f) => f.tag).join("、") || "(无终产物)"};归档 ${result.archivedTags.length} 个过程 tag。下一阶段以定稿为准。`,
|
||||
);
|
||||
@@ -814,6 +876,15 @@ export class PhaseRuntime {
|
||||
case "propose_next_creation_step":
|
||||
await this.proposeNextCreationStep();
|
||||
break;
|
||||
case "seal_creation_opening":
|
||||
this.sealCreationOpening();
|
||||
break;
|
||||
case "run_play_turn":
|
||||
await this.startPlayTurn();
|
||||
break;
|
||||
case "continue_play_turn":
|
||||
await this.continuePlayTurn();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,6 +893,10 @@ export class PhaseRuntime {
|
||||
* 用户填完「下一步想写什么」后:有待执行步则提案并进入确认;否则交总管扩步/收口。
|
||||
*/
|
||||
private async proposeNextCreationStep(): Promise<void> {
|
||||
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
|
||||
this.onMessage(CREATION_SEALED_WAITING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
this.syncSlotsToBlackboard(this.session);
|
||||
const flow = parseCreationFlow(
|
||||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||
@@ -853,6 +928,9 @@ export class PhaseRuntime {
|
||||
const active = this.getActiveSkill();
|
||||
let paramSpecs: ModuleParamSpec[] = [];
|
||||
let declaration = "";
|
||||
let auto = false;
|
||||
let kind: ModuleCatalogEntry["kind"];
|
||||
let pendingModule: ModuleCatalogEntry | null = null;
|
||||
if (active?.name) {
|
||||
try {
|
||||
const skill = await loadSkill(active.name);
|
||||
@@ -861,6 +939,9 @@ export class PhaseRuntime {
|
||||
const mod = catalog?.modules.find((m) => m.name === pending.name);
|
||||
paramSpecs = mod?.params ?? [];
|
||||
declaration = mod?.declaration?.trim() || "";
|
||||
auto = mod?.auto === true;
|
||||
kind = mod?.kind;
|
||||
pendingModule = mod ?? null;
|
||||
}
|
||||
} catch {
|
||||
/* catalog optional */
|
||||
@@ -870,13 +951,7 @@ export class PhaseRuntime {
|
||||
debugLog("step", `提案下一步 ${pending.id}(${pending.name})`);
|
||||
|
||||
const params = pending.params ? { ...pending.params } : {};
|
||||
const paramsMissing = paramSpecs
|
||||
.filter((p) => p.required)
|
||||
.map((p) => p.key)
|
||||
.filter((key) => {
|
||||
const v = params[key];
|
||||
return v == null || (typeof v === "string" && !v.trim());
|
||||
});
|
||||
const paramsMissing = missingRequiredStepParams(pending, pendingModule);
|
||||
|
||||
const intent =
|
||||
typeof this.session.slots["用户.下一步意向"] === "string"
|
||||
@@ -890,8 +965,9 @@ export class PhaseRuntime {
|
||||
: paramsMissing.length
|
||||
? `;待钉参数:${paramsMissing.join("、")}`
|
||||
: "";
|
||||
const revise = isReviseStep(pending);
|
||||
const reason = [
|
||||
`下一步:${pending.name}`,
|
||||
revise ? `下一步:回头修改 · ${pending.name}` : `下一步:${pending.name}`,
|
||||
declaration ? `—— ${declaration}` : "",
|
||||
paramHint,
|
||||
intent ? `(你的意向:${intent})` : "",
|
||||
@@ -912,6 +988,9 @@ export class PhaseRuntime {
|
||||
hint: p.hint,
|
||||
})),
|
||||
intent: intent || undefined,
|
||||
...(kind ? { kind } : {}),
|
||||
...(revise ? { mode: "revise" as const } : {}),
|
||||
...(pending.revises ? { revises: pending.revises } : {}),
|
||||
};
|
||||
this.blackboard.write({
|
||||
tag: CREATION_PROPOSED_STEP_TAG,
|
||||
@@ -930,7 +1009,8 @@ export class PhaseRuntime {
|
||||
action: "run_worker",
|
||||
workerId: "design-step",
|
||||
reason,
|
||||
requiresApproval: true,
|
||||
// 程序步(如投影排序)且参数已齐:跳过「同意并开始」,直接执行。
|
||||
requiresApproval: !(auto && paramsMissing.length === 0),
|
||||
});
|
||||
await this.dispatch({
|
||||
type: "main_agent_decision_created",
|
||||
@@ -938,6 +1018,81 @@ export class PhaseRuntime {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 选定开场白后:把候选落成 输出.开场白 / 初值表,关闭 DAG。
|
||||
*/
|
||||
private sealCreationOpening(): void {
|
||||
const selectedRaw = this.session.slots[SLOT_OPENING_SELECTED_INDEX];
|
||||
const selectedIndex =
|
||||
typeof selectedRaw === "number"
|
||||
? selectedRaw
|
||||
: typeof selectedRaw === "string" && selectedRaw.trim()
|
||||
? Number(selectedRaw)
|
||||
: 0;
|
||||
|
||||
const fragmentRaw =
|
||||
this.blackboard.getContentByTag(OPENING_SETUP_ARTIFACT_TAG) ?? "";
|
||||
const payload = parseOpeningSealPayload(fragmentRaw, selectedIndex);
|
||||
const existingOpening = this.blackboard.getContentByTag(OPENING_OUTPUT_TAG)?.trim();
|
||||
const openingText = payload?.selectedText?.trim() || existingOpening || "";
|
||||
|
||||
if (openingText) {
|
||||
this.writeWorkerTagContent(OPENING_OUTPUT_TAG, openingText, "opening-setup");
|
||||
}
|
||||
|
||||
if (payload?.variables.length) {
|
||||
const source = "worker:opening-setup";
|
||||
const initialPatch = mergeOpeningTablePatch(
|
||||
this.blackboard.getContentByTag(OPENING_INITIAL_VARS_TAG),
|
||||
payload.variables,
|
||||
source,
|
||||
);
|
||||
if (initialPatch) {
|
||||
this.writeWorkerTagContent(OPENING_INITIAL_VARS_TAG, initialPatch, "opening-setup");
|
||||
}
|
||||
const currentPatch = mergeOpeningTablePatch(
|
||||
this.blackboard.getContentByTag(OPENING_CURRENT_VARS_TAG),
|
||||
payload.variables,
|
||||
source,
|
||||
);
|
||||
if (currentPatch) {
|
||||
this.writeWorkerTagContent(OPENING_CURRENT_VARS_TAG, currentPatch, "opening-setup");
|
||||
}
|
||||
}
|
||||
|
||||
const closed = closeCreationFlowRaw(
|
||||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||
);
|
||||
if (closed) {
|
||||
this.blackboard.write({
|
||||
tag: CREATION_FLOW_TAG,
|
||||
content: closed,
|
||||
source: "runtime",
|
||||
});
|
||||
}
|
||||
|
||||
const n = payload?.candidates.length ?? (openingText ? 1 : 0);
|
||||
const which =
|
||||
n > 1 && payload
|
||||
? `第 ${payload.selectedIndex + 1}/${n} 条`
|
||||
: n
|
||||
? "开场白"
|
||||
: "开场";
|
||||
this.onMessage(
|
||||
`[创作收口] 已选定${which},工作流计划已关闭。可切换到「游玩」。`,
|
||||
);
|
||||
|
||||
this.session = {
|
||||
...this.session,
|
||||
slots: {
|
||||
...this.session.slots,
|
||||
[SLOT_CREATION_SEALED_BY_OPENING]: true,
|
||||
[CREATION_FLOW_TAG]: closed ?? this.session.slots[CREATION_FLOW_TAG],
|
||||
[OPENING_OUTPUT_TAG]: openingText || this.session.slots[OPENING_OUTPUT_TAG],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private applyProposedStepParams(params: CreationFlowStepParams): void {
|
||||
const raw =
|
||||
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||||
@@ -947,6 +1102,7 @@ export class PhaseRuntime {
|
||||
if (!raw?.trim()) return;
|
||||
let snap: {
|
||||
stepId?: string;
|
||||
kind?: string;
|
||||
paramSpecs?: Array<{ key: string; required?: boolean }>;
|
||||
};
|
||||
try {
|
||||
@@ -967,13 +1123,16 @@ export class PhaseRuntime {
|
||||
content,
|
||||
source: "user",
|
||||
});
|
||||
const paramsMissing = (snap.paramSpecs ?? [])
|
||||
.filter((p) => p.required)
|
||||
.map((p) => p.key)
|
||||
.filter((key) => {
|
||||
const v = params[key];
|
||||
return v == null || (typeof v === "string" && !v.trim());
|
||||
});
|
||||
const paramsMissing =
|
||||
snap.kind === "prior-artifact"
|
||||
? []
|
||||
: (snap.paramSpecs ?? [])
|
||||
.filter((p) => p.required)
|
||||
.map((p) => p.key)
|
||||
.filter((key) => {
|
||||
const v = params[key];
|
||||
return v == null || (typeof v === "string" && !v.trim());
|
||||
});
|
||||
const snapshot = {
|
||||
...snap,
|
||||
params,
|
||||
@@ -1122,6 +1281,7 @@ export class PhaseRuntime {
|
||||
this.onMessage(
|
||||
`[Worker] ${workerId} 执行中…${resolved.source === "declaration" ? "(声明驱动)" : ""}`,
|
||||
);
|
||||
debugLog("step", `调用模型 ${workerId}`);
|
||||
this.onWorkerStreamStart?.(workerId);
|
||||
|
||||
try {
|
||||
@@ -1131,10 +1291,7 @@ export class PhaseRuntime {
|
||||
slots,
|
||||
blackboard: this.blackboard,
|
||||
llm: workerLlm,
|
||||
declared:
|
||||
resolved.source === "declaration"
|
||||
? { worker: resolved.worker, promptBody: resolved.promptBody }
|
||||
: undefined,
|
||||
declared: { worker: resolved.worker, promptBody: resolved.promptBody },
|
||||
stream: {
|
||||
onThinkingDelta: (delta) =>
|
||||
this.onWorkerThinkingDelta?.(workerId, delta),
|
||||
@@ -1193,8 +1350,13 @@ export class PhaseRuntime {
|
||||
});
|
||||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||||
|
||||
const hasPlayVisible = Object.keys(result.outputs).some(
|
||||
(t) => t === "输出.用户展示" || t === "输出.开场白",
|
||||
);
|
||||
this.onMessage(
|
||||
`[Worker] ${workerId} 已完成\n\n${result.preview}${result.preview.length >= 4000 ? "\n\n…" : ""}`,
|
||||
`[Worker] ${workerId} 已完成\n\n${result.preview}${
|
||||
!hasPlayVisible && result.preview.length >= 4000 ? "\n\n…" : ""
|
||||
}`,
|
||||
);
|
||||
|
||||
await this.dispatch({
|
||||
@@ -1907,8 +2069,59 @@ export class PhaseRuntime {
|
||||
);
|
||||
}
|
||||
|
||||
/** 游玩:按 playWorkerIds 开一轮 */
|
||||
private async startPlayTurn(): Promise<void> {
|
||||
this.syncSlotsToBlackboard(this.session);
|
||||
const ids = this.getInstanceWorkerDeclaration().playWorkerIds;
|
||||
if (!ids.length) {
|
||||
this.onMessage(
|
||||
"[游玩] 运行规格里没有可上场的执行单元,无法推进回合。",
|
||||
);
|
||||
this.waitForPlayInput();
|
||||
return;
|
||||
}
|
||||
const [first, ...rest] = ids;
|
||||
this.session = {
|
||||
...this.session,
|
||||
slots: withPlayTurnQueue(this.session.slots, rest),
|
||||
};
|
||||
await this.processEffects([{ type: "run_worker", workerId: first }]);
|
||||
}
|
||||
|
||||
/** 游玩:跑管线下一个;队空则等用户 */
|
||||
private async continuePlayTurn(): Promise<void> {
|
||||
if (this.session.phase === "waiting_user") return;
|
||||
const queue = readPlayTurnQueue(this.session.slots);
|
||||
if (!queue.length) {
|
||||
this.waitForPlayInput();
|
||||
return;
|
||||
}
|
||||
const [next, ...rest] = queue;
|
||||
this.session = {
|
||||
...this.session,
|
||||
slots: withPlayTurnQueue(this.session.slots, rest),
|
||||
};
|
||||
await this.processEffects([{ type: "run_worker", workerId: next }]);
|
||||
}
|
||||
|
||||
private waitForPlayInput(): void {
|
||||
this.session = {
|
||||
...this.session,
|
||||
phase: "waiting_user",
|
||||
waitingReason: { kind: "input" },
|
||||
currentWorkerId: undefined,
|
||||
pendingArtifactId: undefined,
|
||||
pendingDecision: undefined,
|
||||
resumeContext: undefined,
|
||||
slots: withPlayTurnQueue(this.session.slots, undefined),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** invoke_main_agent 副作用:running 时调用总管 LLM,链式推进直到需用户介入 */
|
||||
private async maybeRunMainAgent(effects: PhaseEffect[]): Promise<void> {
|
||||
if (inferLifecycleStage(this.session) === "play") return;
|
||||
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) return;
|
||||
if (this.mainAgent && effects.some((e) => e.type === "invoke_main_agent")) {
|
||||
await this.runMainAgent();
|
||||
}
|
||||
|
||||
@@ -399,7 +399,10 @@ export function buildFocus(
|
||||
return {
|
||||
actorType: "user",
|
||||
actorLabel: "你",
|
||||
action: "补充说明",
|
||||
action:
|
||||
reason.message && /创作已收口/.test(reason.message)
|
||||
? "创作已收口"
|
||||
: "补充说明",
|
||||
detail: reason.message?.slice(0, 120),
|
||||
};
|
||||
}
|
||||
@@ -412,6 +415,7 @@ export function buildFocus(
|
||||
try {
|
||||
return JSON.parse(String(session.slots["创作.待确认步骤"])) as {
|
||||
name?: string;
|
||||
mode?: string;
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -423,7 +427,9 @@ export function buildFocus(
|
||||
actorId: "orchestrator",
|
||||
actorLabel: "编排",
|
||||
action: proposed?.name
|
||||
? `确认开始 · ${proposed.name}`
|
||||
? proposed.mode === "revise"
|
||||
? `回头修改 · ${proposed.name}`
|
||||
: `确认开始 · ${proposed.name}`
|
||||
: `建议调用 ${displayWorkerLabel(worker)}`,
|
||||
detail: session.pendingDecision?.reason,
|
||||
};
|
||||
@@ -467,6 +473,7 @@ export function buildFocus(
|
||||
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
||||
const copy = reviewComposerCopy(art?.workerId, {
|
||||
hasQuestions: Boolean(reason.questions?.length),
|
||||
outputTags: art?.outputTags,
|
||||
});
|
||||
const optionalQs = reason.questions?.length
|
||||
? `;另有 ${reason.questions.length} 道可选追问`
|
||||
|
||||
@@ -146,10 +146,13 @@ export type ReviewComposerCopy = {
|
||||
*/
|
||||
export function reviewComposerCopy(
|
||||
workerId: string | undefined | null,
|
||||
opts?: { hasQuestions?: boolean },
|
||||
opts?: { hasQuestions?: boolean; outputTags?: readonly string[] },
|
||||
): ReviewComposerCopy {
|
||||
const hasQuestions = Boolean(opts?.hasQuestions);
|
||||
const id = (workerId ?? "").trim();
|
||||
const openingReview =
|
||||
id === "opening-generator" ||
|
||||
(opts?.outputTags ?? []).includes("设计.开场白与开场变量");
|
||||
|
||||
if (id === "design-flow") {
|
||||
return {
|
||||
@@ -169,7 +172,7 @@ export function reviewComposerCopy(
|
||||
};
|
||||
}
|
||||
|
||||
if (id === "opening-generator") {
|
||||
if (id === "opening-generator" || openingReview) {
|
||||
return {
|
||||
chip: "开场",
|
||||
kicker: "待选定",
|
||||
|
||||
+540
-78
@@ -10,6 +10,22 @@ import {
|
||||
import type { RunSnapshot, RunSnapshotMeta, SnapshotKind } from "../types/run-snapshot.js";
|
||||
import { toRunSnapshotMeta } from "../types/run-snapshot.js";
|
||||
import { materializeInstanceSnapshotPayload } from "../book/snapshot-filters.js";
|
||||
import {
|
||||
CREATION_SEALED_WAITING_MESSAGE,
|
||||
INSTANCE_OPENING_SNAPSHOT_LABEL,
|
||||
OPENING_OUTPUT_TAG,
|
||||
SLOT_CREATION_SEALED_BY_OPENING,
|
||||
} from "../skills/opening-seal.js";
|
||||
import {
|
||||
PLAY_WORKING_SNAPSHOT_ID,
|
||||
PLAY_WORKING_SNAPSHOT_LABEL,
|
||||
SLOT_PLAY_LAYER_ACTIVE,
|
||||
isPlayLayerActive,
|
||||
} from "../skills/play-turn.js";
|
||||
import {
|
||||
DIALOGUE_HISTORY_TAG,
|
||||
appendDialogueHistoryTurn,
|
||||
} from "../skills/dialogue-history.js";
|
||||
import { PhaseRuntime, createDecision } from "../runtime/phase-runtime.js";
|
||||
import { createSession } from "../runtime/phase-machine.js";
|
||||
import {
|
||||
@@ -18,6 +34,7 @@ import {
|
||||
reloadDefaultMainAgentLlm,
|
||||
type LlmTrackingRef,
|
||||
} from "../runtime/llm-factory.js";
|
||||
import { isAbortError, runWithAbortSignal } from "../llm/run-abort.js";
|
||||
import { getSessionTokenTotals, type MessageTokenUsage } from "../stats/token-store.js";
|
||||
import type { PersistedBookSession } from "../types/book-session.js";
|
||||
import type { RuntimeSession, WaitingReason } from "../types/runtime.js";
|
||||
@@ -179,6 +196,8 @@ export type SessionView = {
|
||||
/** @deprecated 用 skillCatalog */
|
||||
pipeline: StageStep[];
|
||||
lifecycleStage: LifecycleStage;
|
||||
/** 已从定稿开出聊天层(不是只把顶栏标成游玩) */
|
||||
playLayerActive: boolean;
|
||||
playReady: boolean;
|
||||
skillCatalog: SkillCatalogEntry[];
|
||||
toolTrace: ToolTraceEntry[];
|
||||
@@ -271,7 +290,13 @@ export type ProposedNextStepView = {
|
||||
required: boolean;
|
||||
hint?: string;
|
||||
}>;
|
||||
/** 节点特性:prior-artifact = 先验产物(不在确认开干时钉「写什么」) */
|
||||
kind?: string;
|
||||
/** 用户在「下一步想写什么」里写的意向(与步骤 mode 不同) */
|
||||
intent?: string;
|
||||
/** 步骤怎么跑:revise = 回头修改既有产物 */
|
||||
mode?: "fresh" | "revise";
|
||||
revises?: string;
|
||||
};
|
||||
|
||||
export type ReviewArtifactView = {
|
||||
@@ -279,6 +304,7 @@ export type ReviewArtifactView = {
|
||||
workerId: string;
|
||||
summary?: string;
|
||||
body: string;
|
||||
outputTags?: string[];
|
||||
/** 产出该产物的 worker_output 消息(验收态下 feed 会隐藏该消息) */
|
||||
sourceMessageId?: string;
|
||||
/** 与 sourceMessage 同源的全量 LLM 上下文,供待验收卡片右键查看 */
|
||||
@@ -301,6 +327,12 @@ type ManagedSession = {
|
||||
recipeOptions?: RecipeCatalogEntry[];
|
||||
/** 开局模块 opening,缓存给意图页 */
|
||||
openingGuide?: { stepId: string; stepName: string; text: string } | null;
|
||||
/** 当前 LLM 请求的取消器(停止并重试) */
|
||||
runAbort?: AbortController;
|
||||
/** 递增世代:后一次请求作废前一次 */
|
||||
runGen?: number;
|
||||
/** 本轮开始时的消息条数,取消时裁掉执行中产生的调度句 */
|
||||
runMessageCutoff?: number;
|
||||
};
|
||||
|
||||
export class SessionManager {
|
||||
@@ -327,7 +359,11 @@ export class SessionManager {
|
||||
if (stage === "play" && !canEnterPlay(session)) {
|
||||
throw new Error("实例尚未就绪,无法进入游玩");
|
||||
}
|
||||
s.runtime.setLifecycleStage(stage);
|
||||
if (stage === "play") {
|
||||
this.enterPlayLayer(s, { resume: true });
|
||||
} else {
|
||||
this.enterDesignLayer(s);
|
||||
}
|
||||
if (s.bookId) this.persist(s);
|
||||
return this.toView(sessionId);
|
||||
}
|
||||
@@ -582,10 +618,10 @@ export class SessionManager {
|
||||
return toRunSnapshotMeta(snapshot);
|
||||
}
|
||||
|
||||
/** 列出某 Book 的全部存档 */
|
||||
/** 列出某 Book 的全部存档(不含内部「当前游玩」工作副本) */
|
||||
listGameSnapshots(bookId: string): RunSnapshotMeta[] {
|
||||
if (!getBook(bookId)) throw new Error("Book 不存在");
|
||||
return listRunSnapshots(bookId);
|
||||
return listRunSnapshots(bookId).filter((s) => s.id !== PLAY_WORKING_SNAPSHOT_ID);
|
||||
}
|
||||
|
||||
/** 仅游玩 run 存档(UI 抽屉用) */
|
||||
@@ -604,7 +640,7 @@ export class SessionManager {
|
||||
if (!canEnterPlay(session)) {
|
||||
throw new Error("须先验收 Worker 集,才能保存游玩存档");
|
||||
}
|
||||
if (inferLifecycleStage(session) !== "play") {
|
||||
if (inferLifecycleStage(session) !== "play" || !isPlayLayerActive(session.slots)) {
|
||||
throw new Error("请先切换到「游玩」再保存游玩存档");
|
||||
}
|
||||
return this.saveGameSnapshot(sessionId, label, "run", note);
|
||||
@@ -618,16 +654,97 @@ export class SessionManager {
|
||||
if (!canEnterPlay(session)) {
|
||||
throw new Error("须先验收 Worker 集,才能开始游玩");
|
||||
}
|
||||
this.archivePlayWorking(s.bookId);
|
||||
this.enterPlayLayer(s, { resume: false });
|
||||
this.persist(s);
|
||||
return this.toView(sessionId);
|
||||
}
|
||||
|
||||
let runtimeSession = structuredClone(session);
|
||||
/** 进入游玩层:resume 时恢复「当前游玩」,否则从定稿新开一条聊天 */
|
||||
private enterPlayLayer(
|
||||
s: ManagedSession,
|
||||
opts: { resume: boolean },
|
||||
): void {
|
||||
const session = s.runtime.getSession();
|
||||
if (!canEnterPlay(session)) {
|
||||
throw new Error("实例尚未就绪,无法进入游玩");
|
||||
}
|
||||
if (
|
||||
opts.resume &&
|
||||
isPlayLayerActive(session.slots) &&
|
||||
inferLifecycleStage(session) === "play"
|
||||
) {
|
||||
this.sanitizePlayHitl(s);
|
||||
return;
|
||||
}
|
||||
if (s.bookId) this.ensureInstanceSnapshot(s);
|
||||
if (opts.resume && s.bookId) {
|
||||
const working = loadRunSnapshotFile(s.bookId, PLAY_WORKING_SNAPSHOT_ID);
|
||||
if (working?.kind === "run") {
|
||||
this.restoreSnapshotInPlace(s, working, "play");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.beginNewPlayFromInstance(s);
|
||||
if (s.bookId) this.savePlayWorking(s);
|
||||
}
|
||||
|
||||
/** 回到创作层:先把当前聊天写入「当前游玩」,再加载定稿 */
|
||||
private enterDesignLayer(s: ManagedSession): void {
|
||||
const session = s.runtime.getSession();
|
||||
if (
|
||||
inferLifecycleStage(session) === "design" &&
|
||||
!isPlayLayerActive(session.slots)
|
||||
) {
|
||||
s.runtime.setLifecycleStage("design");
|
||||
return;
|
||||
}
|
||||
if (s.bookId && isPlayLayerActive(session.slots)) {
|
||||
this.savePlayWorking(s);
|
||||
}
|
||||
const instance = s.bookId ? this.latestInstanceSnapshot(s.bookId) : null;
|
||||
if (instance) {
|
||||
this.restoreSnapshotInPlace(s, instance, "design");
|
||||
return;
|
||||
}
|
||||
let runtimeSession = structuredClone(s.runtime.getSession());
|
||||
let blackboardItems = s.runtime.getBlackboard().exportItems();
|
||||
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
||||
runtimeSession,
|
||||
blackboardItems,
|
||||
}));
|
||||
runtimeSession.id = session.id;
|
||||
runtimeSession.slots = {
|
||||
...runtimeSession.slots,
|
||||
uiLifecycleStage: "design",
|
||||
[SLOT_PLAY_LAYER_ACTIVE]: undefined,
|
||||
};
|
||||
runtimeSession.phase = "waiting_user";
|
||||
runtimeSession.waitingReason = { kind: "input" };
|
||||
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||
}
|
||||
|
||||
private beginNewPlayFromInstance(s: ManagedSession): void {
|
||||
const keepId = s.runtime.getSession().id;
|
||||
let runtimeSession: RuntimeSession;
|
||||
let blackboardItems: import("../types/blackboard.js").BlackboardItem[];
|
||||
const instance = s.bookId ? this.latestInstanceSnapshot(s.bookId) : null;
|
||||
if (instance) {
|
||||
runtimeSession = structuredClone(instance.runtimeSession);
|
||||
blackboardItems = structuredClone(instance.blackboardItems);
|
||||
} else {
|
||||
runtimeSession = structuredClone(s.runtime.getSession());
|
||||
blackboardItems = s.runtime.getBlackboard().exportItems();
|
||||
}
|
||||
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
||||
runtimeSession,
|
||||
blackboardItems,
|
||||
}));
|
||||
runtimeSession.id = keepId;
|
||||
runtimeSession.slots = {
|
||||
...runtimeSession.slots,
|
||||
uiLifecycleStage: "play",
|
||||
[SLOT_PLAY_LAYER_ACTIVE]: true,
|
||||
startupCompleted: true,
|
||||
};
|
||||
runtimeSession.phase = "waiting_user";
|
||||
@@ -636,10 +753,166 @@ export class SessionManager {
|
||||
runtimeSession.pendingDecision = undefined;
|
||||
runtimeSession.currentWorkerId = undefined;
|
||||
runtimeSession.resumeContext = undefined;
|
||||
|
||||
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||
this.persist(s);
|
||||
return this.toView(sessionId);
|
||||
const opening =
|
||||
s.runtime.getBlackboard().getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||
if (opening) {
|
||||
s.runtime.getBlackboard().write({
|
||||
tag: DIALOGUE_HISTORY_TAG,
|
||||
content: appendDialogueHistoryTurn("", { role: "助手", text: opening }),
|
||||
source: "runtime",
|
||||
});
|
||||
s.messages = [this.playOpeningMessage(opening)];
|
||||
} else {
|
||||
s.messages = [];
|
||||
}
|
||||
s.branchState = createMessageBranchState();
|
||||
}
|
||||
|
||||
private restoreSnapshotInPlace(
|
||||
s: ManagedSession,
|
||||
snapshot: RunSnapshot,
|
||||
layer: LifecycleStage,
|
||||
): void {
|
||||
const keepId = s.runtime.getSession().id;
|
||||
let runtimeSession = structuredClone(snapshot.runtimeSession);
|
||||
let blackboardItems = structuredClone(snapshot.blackboardItems);
|
||||
if (snapshot.kind === "instance" || layer === "design") {
|
||||
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
|
||||
runtimeSession,
|
||||
blackboardItems,
|
||||
}));
|
||||
}
|
||||
runtimeSession.id = keepId;
|
||||
runtimeSession.slots = {
|
||||
...runtimeSession.slots,
|
||||
uiLifecycleStage: layer,
|
||||
[SLOT_PLAY_LAYER_ACTIVE]: layer === "play" ? true : undefined,
|
||||
startupCompleted: true,
|
||||
};
|
||||
if (layer === "play") {
|
||||
runtimeSession.phase = runtimeSession.phase === "done"
|
||||
? "waiting_user"
|
||||
: runtimeSession.phase;
|
||||
if (!runtimeSession.waitingReason) {
|
||||
runtimeSession.waitingReason = { kind: "input" };
|
||||
}
|
||||
} else {
|
||||
const sealed = Boolean(runtimeSession.slots[SLOT_CREATION_SEALED_BY_OPENING]);
|
||||
runtimeSession.phase = "waiting_user";
|
||||
runtimeSession.waitingReason = {
|
||||
kind: "input",
|
||||
...(sealed ? { message: CREATION_SEALED_WAITING_MESSAGE } : {}),
|
||||
};
|
||||
runtimeSession.pendingArtifactId = undefined;
|
||||
runtimeSession.pendingDecision = undefined;
|
||||
runtimeSession.currentWorkerId = undefined;
|
||||
}
|
||||
s.runtime.restoreFromCheckpoint(runtimeSession, blackboardItems);
|
||||
s.messages = snapshot.messages.map((m) => ({ ...m })) as ChatMessage[];
|
||||
s.branchState = createMessageBranchState();
|
||||
if (layer === "play") this.sanitizePlayHitl(s);
|
||||
}
|
||||
|
||||
/** 游玩层不保留创作 HITL(验收 / 确认下一步 / 修订) */
|
||||
private sanitizePlayHitl(s: ManagedSession): void {
|
||||
const session = s.runtime.getSession();
|
||||
const play =
|
||||
isPlayLayerActive(session.slots) || inferLifecycleStage(session) === "play";
|
||||
if (!play) return;
|
||||
const kind = session.waitingReason?.kind;
|
||||
if (
|
||||
kind !== "review_artifact" &&
|
||||
kind !== "approve_step" &&
|
||||
kind !== "revision" &&
|
||||
kind !== "next_intent"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
session.phase = "waiting_user";
|
||||
session.waitingReason = { kind: "input" };
|
||||
session.pendingArtifactId = undefined;
|
||||
session.pendingDecision = undefined;
|
||||
session.currentWorkerId = undefined;
|
||||
session.resumeContext = undefined;
|
||||
session.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
private savePlayWorking(s: ManagedSession): void {
|
||||
if (!s.bookId) return;
|
||||
const book = getBook(s.bookId);
|
||||
const runtimeSession = structuredClone(s.runtime.getSession());
|
||||
const snapshot: RunSnapshot = {
|
||||
version: 1,
|
||||
id: PLAY_WORKING_SNAPSHOT_ID,
|
||||
bookId: s.bookId,
|
||||
label: PLAY_WORKING_SNAPSHOT_LABEL,
|
||||
kind: "run",
|
||||
orchestratorId:
|
||||
sessionSkillPackId(runtimeSession) ??
|
||||
(book ? bookSkillPackId(book) : "") ??
|
||||
"",
|
||||
runtimeSession,
|
||||
blackboardItems: s.runtime.getBlackboard().exportItems(),
|
||||
messages: s.messages.map((m) => ({ ...m })),
|
||||
createdAt: new Date().toISOString(),
|
||||
note: "切换创作/游玩时自动保存的当前聊天",
|
||||
};
|
||||
saveRunSnapshotFile(snapshot);
|
||||
}
|
||||
|
||||
private archivePlayWorking(bookId: string): void {
|
||||
const working = loadRunSnapshotFile(bookId, PLAY_WORKING_SNAPSHOT_ID);
|
||||
if (!working) return;
|
||||
if (!working.messages.some((m) => m.role === "user")) return;
|
||||
const stamped: RunSnapshot = {
|
||||
...working,
|
||||
id: randomUUID(),
|
||||
label: `游玩 ${new Date().toLocaleString("zh-CN", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
note: "新建游玩前自动保存",
|
||||
};
|
||||
saveRunSnapshotFile(stamped);
|
||||
}
|
||||
|
||||
private ensureInstanceSnapshot(s: ManagedSession): void {
|
||||
if (!s.bookId) return;
|
||||
if (this.latestInstanceSnapshot(s.bookId)) return;
|
||||
if (!canEnterPlay(s.runtime.getSession())) return;
|
||||
try {
|
||||
this.saveGameSnapshot(
|
||||
s.runtime.getSession().id,
|
||||
INSTANCE_OPENING_SNAPSHOT_LABEL,
|
||||
"instance",
|
||||
"进入游玩前自动保存定稿",
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn("[会话] 进入游玩前保存定稿失败", err);
|
||||
}
|
||||
}
|
||||
|
||||
private latestInstanceSnapshot(bookId: string): RunSnapshot | null {
|
||||
const meta = this.listGameSnapshots(bookId).find((s) => s.kind === "instance");
|
||||
if (!meta) return null;
|
||||
return loadRunSnapshotFile(bookId, meta.id);
|
||||
}
|
||||
|
||||
private playOpeningMessage(text: string): ChatMessage {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
role: "system",
|
||||
text,
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: "system_info",
|
||||
actor: "narrator",
|
||||
title: "开场白",
|
||||
body: text,
|
||||
};
|
||||
}
|
||||
|
||||
/** 从存档读档:替换当前作品进度,可继续创作 */
|
||||
@@ -665,10 +938,16 @@ export class SessionManager {
|
||||
runtimeSession,
|
||||
blackboardItems,
|
||||
}));
|
||||
runtimeSession.slots = {
|
||||
...runtimeSession.slots,
|
||||
uiLifecycleStage: "design",
|
||||
[SLOT_PLAY_LAYER_ACTIVE]: undefined,
|
||||
};
|
||||
} else {
|
||||
runtimeSession.slots = {
|
||||
...runtimeSession.slots,
|
||||
uiLifecycleStage: "play",
|
||||
[SLOT_PLAY_LAYER_ACTIVE]: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -725,6 +1004,16 @@ export class SessionManager {
|
||||
opts?: { answers?: QuestionAnswer[] },
|
||||
): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
this.sanitizePlayHitl(s);
|
||||
const waitingBefore = s.runtime.getSession().waitingReason;
|
||||
if (
|
||||
inferLifecycleStage(s.runtime.getSession()) === "play" &&
|
||||
!isPlayLayerActive(s.runtime.getSession().slots) &&
|
||||
waitingBefore?.kind === "input"
|
||||
) {
|
||||
this.enterPlayLayer(s, { resume: true });
|
||||
if (s.bookId) this.persist(s);
|
||||
}
|
||||
this.clearAgentThinking(id);
|
||||
recordPreMessageCheckpoint(
|
||||
s.branchState,
|
||||
@@ -764,35 +1053,36 @@ export class SessionManager {
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
}
|
||||
try {
|
||||
if (reason?.kind === "approve_step") {
|
||||
await s.runtime.rejectStep(trimmed || text);
|
||||
} else if (reason?.kind === "review_artifact") {
|
||||
// 先把卡上追问(问+选项+补充)写入「用户.worker答复」,再按意见改产物
|
||||
if (sidecarAnswers.length && reason.questions?.length) {
|
||||
const qaText = formatQuestionAnswersForAi(
|
||||
normalizeQuestions(reason.questions),
|
||||
sidecarAnswers,
|
||||
trimmed || undefined,
|
||||
);
|
||||
await s.runtime.resolveSidecarQuestions(qaText);
|
||||
const owned = await this.runExclusive(id, s, async () => {
|
||||
if (reason?.kind === "approve_step") {
|
||||
await s.runtime.rejectStep(trimmed || text);
|
||||
} else if (reason?.kind === "review_artifact") {
|
||||
// 先把卡上追问(问+选项+补充)写入「用户.worker答复」,再按意见改产物
|
||||
if (sidecarAnswers.length && reason.questions?.length) {
|
||||
const qaText = formatQuestionAnswersForAi(
|
||||
normalizeQuestions(reason.questions),
|
||||
sidecarAnswers,
|
||||
trimmed || undefined,
|
||||
);
|
||||
await s.runtime.resolveSidecarQuestions(qaText);
|
||||
}
|
||||
if (trimmed || sidecarAnswers.length) {
|
||||
await s.runtime.rejectArtifact(
|
||||
trimmed || "按追问作答更新产物",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await s.runtime.submitInput(trimmed || text);
|
||||
}
|
||||
if (trimmed || sidecarAnswers.length) {
|
||||
await s.runtime.rejectArtifact(
|
||||
trimmed || "按追问作答更新产物",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await s.runtime.submitInput(trimmed || text);
|
||||
}
|
||||
});
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[会话] 发送消息失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
} finally {
|
||||
this.clearAgentThinking(id);
|
||||
this.syncCreationDialogue(s);
|
||||
}
|
||||
this.syncCreationDialogue(s);
|
||||
this.persist(s);
|
||||
return this.toView(id);
|
||||
}
|
||||
@@ -838,22 +1128,23 @@ export class SessionManager {
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
|
||||
try {
|
||||
if (isSidecar) {
|
||||
await s.runtime.resolveSidecarQuestions(aiText);
|
||||
// 作答必须写回产物,否则验收后进度空转、追问在美学/交互间打转
|
||||
await s.runtime.rejectArtifact("按追问作答更新产物");
|
||||
} else {
|
||||
await s.runtime.submitInput(aiText);
|
||||
}
|
||||
const owned = await this.runExclusive(id, s, async () => {
|
||||
if (isSidecar) {
|
||||
await s.runtime.resolveSidecarQuestions(aiText);
|
||||
// 作答必须写回产物,否则验收后进度空转、追问在美学/交互间打转
|
||||
await s.runtime.rejectArtifact("按追问作答更新产物");
|
||||
} else {
|
||||
await s.runtime.submitInput(aiText);
|
||||
}
|
||||
});
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[会话] 回答追问失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
} finally {
|
||||
this.clearAgentThinking(id);
|
||||
this.syncCreationDialogue(s);
|
||||
}
|
||||
this.syncCreationDialogue(s);
|
||||
this.persist(s);
|
||||
return this.toView(id);
|
||||
}
|
||||
@@ -872,7 +1163,10 @@ export class SessionManager {
|
||||
this.clearAgentThinking(id);
|
||||
s.messages.push(this.msg("user", "跳过可选追问"));
|
||||
try {
|
||||
await s.runtime.resolveSidecarQuestions();
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.resolveSidecarQuestions(),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
@@ -918,7 +1212,10 @@ export class SessionManager {
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
|
||||
try {
|
||||
await s.runtime.submitInput(trimmed);
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.submitInput(trimmed),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
@@ -974,7 +1271,10 @@ export class SessionManager {
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
|
||||
try {
|
||||
await s.runtime.rerunWorker(workerId);
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.rerunWorker(workerId),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
@@ -997,6 +1297,32 @@ export class SessionManager {
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
/** 中止当前生成并重跑卡住的 worker / 总管 */
|
||||
async abortAndRetry(id: string): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
const session = s.runtime.getSession();
|
||||
if (session.phase !== "running" || session.waitingReason) {
|
||||
throw new Error("当前没有正在执行的任务");
|
||||
}
|
||||
const cutoff = s.runMessageCutoff ?? s.messages.length;
|
||||
s.messages = s.messages.slice(0, cutoff);
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
try {
|
||||
const owned = await this.runExclusive(id, s, async () => {
|
||||
await s.runtime.retryStuckRun();
|
||||
});
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[会话] 停止并重试失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
}
|
||||
this.syncCreationDialogue(s);
|
||||
this.persist(s);
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
/** 删除消息及其后的对话 */
|
||||
async deleteMessage(id: string, messageId: string): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
@@ -1053,7 +1379,10 @@ export class SessionManager {
|
||||
): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
try {
|
||||
await s.runtime.approve(stepParams);
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.approve(stepParams),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[会话] 确认开干失败", detail);
|
||||
@@ -1065,7 +1394,10 @@ export class SessionManager {
|
||||
async confirmIntake(id: string): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
try {
|
||||
await s.runtime.confirmIntake();
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.confirmIntake(),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[会话] 确认需求失败", detail);
|
||||
@@ -1075,7 +1407,10 @@ export class SessionManager {
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
async accept(id: string): Promise<SessionView> {
|
||||
async accept(
|
||||
id: string,
|
||||
opts?: { openingIndex?: number },
|
||||
): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
const pendingId = s.runtime.getSession().pendingArtifactId;
|
||||
const waiting = s.runtime.getSession().waitingReason;
|
||||
@@ -1085,7 +1420,21 @@ export class SessionManager {
|
||||
? s.runtime.getSession().artifacts.find((a) => a.id === pendingId)
|
||||
: undefined;
|
||||
const stageBefore = inferLifecycleStage(s.runtime.getSession());
|
||||
await s.runtime.acceptArtifact();
|
||||
const sealedBefore = Boolean(
|
||||
s.runtime.getSession().slots[SLOT_CREATION_SEALED_BY_OPENING],
|
||||
);
|
||||
try {
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.acceptArtifact(undefined, opts),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[会话] 接受产物失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
this.persist(s);
|
||||
return this.toView(id);
|
||||
}
|
||||
// 接受产物 = 不再用追问完善;询问模块随 waitingReason 清除而收起
|
||||
if (hadSidecarQuestions) {
|
||||
s.messages.push(
|
||||
@@ -1101,6 +1450,8 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
this.persist(s);
|
||||
this.maybeSaveOpeningInstance(s, sealedBefore);
|
||||
this.persist(s);
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
@@ -1109,13 +1460,16 @@ export class SessionManager {
|
||||
const s = this.require(id);
|
||||
const waiting = s.runtime.getSession().waitingReason;
|
||||
try {
|
||||
if (waiting?.kind === "approve_step") {
|
||||
await s.runtime.rejectStep(reason ?? "用户暂不执行");
|
||||
} else if (waiting?.kind === "review_artifact") {
|
||||
await s.runtime.rejectArtifact(reason ?? "用户要求重新来");
|
||||
} else {
|
||||
throw new Error("当前没有可拒绝的确认或验收");
|
||||
}
|
||||
const owned = await this.runExclusive(id, s, async () => {
|
||||
if (waiting?.kind === "approve_step") {
|
||||
await s.runtime.rejectStep(reason ?? "用户暂不执行");
|
||||
} else if (waiting?.kind === "review_artifact") {
|
||||
await s.runtime.rejectArtifact(reason ?? "用户要求重新来");
|
||||
} else {
|
||||
throw new Error("当前没有可拒绝的确认或验收");
|
||||
}
|
||||
});
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[会话] 打回失败", detail);
|
||||
@@ -1127,26 +1481,42 @@ export class SessionManager {
|
||||
|
||||
async runOutline(id: string): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
await s.runtime.submitDecision(
|
||||
createDecision({
|
||||
action: "run_worker",
|
||||
reason: "根据创作简报生成大纲",
|
||||
workerId: "outline",
|
||||
requiresApproval: true,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.submitDecision(
|
||||
createDecision({
|
||||
action: "run_worker",
|
||||
reason: "根据创作简报生成大纲",
|
||||
workerId: "outline",
|
||||
requiresApproval: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
}
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
async finish(id: string): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
await s.runtime.submitDecision(
|
||||
createDecision({
|
||||
action: "finish",
|
||||
reason: "创作流程结束",
|
||||
requiresApproval: false,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const owned = await this.runExclusive(id, s, () =>
|
||||
s.runtime.submitDecision(
|
||||
createDecision({
|
||||
action: "finish",
|
||||
reason: "创作流程结束",
|
||||
requiresApproval: false,
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (!owned) return this.toView(id);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
}
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
@@ -1308,6 +1678,42 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 选定开场后正式保存一份 instance 定稿(可进游玩的规格+开局) */
|
||||
private maybeSaveOpeningInstance(
|
||||
s: ManagedSession,
|
||||
sealedBefore: boolean,
|
||||
): void {
|
||||
if (sealedBefore) return;
|
||||
if (!s.bookId) return;
|
||||
const session = s.runtime.getSession();
|
||||
if (!session.slots[SLOT_CREATION_SEALED_BY_OPENING]) return;
|
||||
if (!canEnterPlay(session)) {
|
||||
s.messages.push(
|
||||
this.msg(
|
||||
"system",
|
||||
"[创作收口] 开场已选定,但 Worker 集尚未验收,未写入创作定稿存档。",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const meta = this.saveGameSnapshot(
|
||||
session.id,
|
||||
INSTANCE_OPENING_SNAPSHOT_LABEL,
|
||||
"instance",
|
||||
"选定开场后自动保存",
|
||||
);
|
||||
s.messages.push(
|
||||
this.msg("system", `[创作收口] 已保存创作定稿「${meta.label}」。`),
|
||||
);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
s.messages.push(
|
||||
this.msg("system", `[创作收口] 开场已选定,定稿存档未写入:${detail}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private persist(s: ManagedSession): void {
|
||||
if (!s.bookId) return;
|
||||
const book = getBook(s.bookId);
|
||||
@@ -1423,7 +1829,7 @@ export class SessionManager {
|
||||
|
||||
private buildLiveStreamView(sessionId: string): LiveStreamView | undefined {
|
||||
const worker = this.workerLive.get(sessionId);
|
||||
if (worker && (worker.thinking || worker.output)) {
|
||||
if (worker) {
|
||||
return {
|
||||
actor: "worker",
|
||||
actorId: worker.workerId,
|
||||
@@ -1449,6 +1855,42 @@ export class SessionManager {
|
||||
return s;
|
||||
}
|
||||
|
||||
private beginRun(s: ManagedSession, sessionId: string): number {
|
||||
s.runAbort?.abort();
|
||||
s.runAbort = new AbortController();
|
||||
s.runGen = (s.runGen ?? 0) + 1;
|
||||
s.runMessageCutoff = s.messages.length;
|
||||
this.clearAgentThinking(sessionId);
|
||||
return s.runGen;
|
||||
}
|
||||
|
||||
private isCurrentRun(s: ManagedSession, gen: number): boolean {
|
||||
return s.runGen === gen;
|
||||
}
|
||||
|
||||
/** true = 本轮仍有效;false = 已被停止并重试取代 */
|
||||
private async runExclusive(
|
||||
id: string,
|
||||
s: ManagedSession,
|
||||
fn: () => Promise<unknown>,
|
||||
): Promise<boolean> {
|
||||
const gen = this.beginRun(s, id);
|
||||
try {
|
||||
await runWithAbortSignal(s.runAbort!.signal, fn);
|
||||
return this.isCurrentRun(s, gen);
|
||||
} catch (err) {
|
||||
if (isAbortError(err) && !this.isCurrentRun(s, gen)) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (this.isCurrentRun(s, gen)) {
|
||||
this.clearAgentThinking(id);
|
||||
s.runAbort = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enrichDisplayMessages(
|
||||
session: RuntimeSession,
|
||||
messages: ChatMessage[],
|
||||
@@ -1595,6 +2037,7 @@ export class SessionManager {
|
||||
|
||||
private toView(id: string, resumed = false, resumeHint?: string): SessionView {
|
||||
const s = this.require(id);
|
||||
this.sanitizePlayHitl(s);
|
||||
const session = s.runtime.getSession();
|
||||
const reason = session.waitingReason;
|
||||
const book = s.bookId ? getBook(s.bookId) : null;
|
||||
@@ -1667,7 +2110,15 @@ export class SessionManager {
|
||||
: "描述你想写什么…",
|
||||
});
|
||||
} else if (reason?.kind === "input") {
|
||||
if (reason.questions?.length) {
|
||||
const playNow =
|
||||
inferLifecycleStage(session) === "play" || isPlayLayerActive(session.slots);
|
||||
if (playNow) {
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "发送",
|
||||
placeholder: "说你要做什么…",
|
||||
});
|
||||
} else if (reason.questions?.length) {
|
||||
const qs = normalizeQuestions(reason.questions);
|
||||
const hasAssessment = Boolean(reason.message?.trim());
|
||||
hints.push(
|
||||
@@ -1732,11 +2183,17 @@ export class SessionManager {
|
||||
);
|
||||
if (proposed) {
|
||||
hints.push(
|
||||
`确认开始「${proposed.name}」${
|
||||
proposed.paramsMissing.length
|
||||
? `(请先补齐:${proposed.paramsMissing.join("、")})`
|
||||
: ""
|
||||
}`,
|
||||
proposed.mode === "revise"
|
||||
? `回头修改「${proposed.name}」${
|
||||
proposed.paramsMissing.length
|
||||
? `(请先补齐:${proposed.paramsMissing.join("、")})`
|
||||
: "(继承既有产物继续改,不是从零再生成)"
|
||||
}`
|
||||
: `确认开始「${proposed.name}」${
|
||||
proposed.paramsMissing.length
|
||||
? `(请先补齐:${proposed.paramsMissing.join("、")})`
|
||||
: ""
|
||||
}`,
|
||||
);
|
||||
actions.push({
|
||||
type: "approve",
|
||||
@@ -1749,10 +2206,10 @@ export class SessionManager {
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "说明意见",
|
||||
placeholder: "不同意可说明要改成什么…",
|
||||
placeholder: "改排、回头改某步,或说明不要这一步…",
|
||||
});
|
||||
} else if (reason?.kind === "next_intent") {
|
||||
hints.push("下一步想写什么?可留空,发送后会展示下一节点供你确认。");
|
||||
hints.push("下一步想写什么?可留空;也可说回头改某步。发送后会展示下一节点供你确认。");
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "继续",
|
||||
@@ -1859,6 +2316,7 @@ export class SessionManager {
|
||||
actions,
|
||||
pipeline: buildPipeline(session),
|
||||
lifecycleStage,
|
||||
playLayerActive: isPlayLayerActive(session.slots),
|
||||
playReady: canEnterPlay(session),
|
||||
skillCatalog,
|
||||
toolTrace: buildToolTrace(messages),
|
||||
@@ -2246,7 +2704,10 @@ function readProposedNextStep(
|
||||
}))
|
||||
.filter((p) => p.key)
|
||||
: [],
|
||||
kind: typeof doc.kind === "string" ? doc.kind : undefined,
|
||||
intent: typeof doc.intent === "string" ? doc.intent : undefined,
|
||||
mode: doc.mode === "revise" ? "revise" : undefined,
|
||||
revises: typeof doc.revises === "string" ? doc.revises : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -2416,6 +2877,7 @@ function buildReviewArtifactView(
|
||||
workerId: art.workerId,
|
||||
summary,
|
||||
body,
|
||||
outputTags: art.outputTags,
|
||||
sourceMessageId: outputMsg?.id,
|
||||
contextTrace: outputMsg?.contextTrace,
|
||||
workerSet,
|
||||
|
||||
@@ -22,12 +22,17 @@ import {
|
||||
patchPresetEntries,
|
||||
type PresetEntryPatch,
|
||||
} from "../preset/entries.js";
|
||||
import { runPresetProbe } from "../preset/probe.js";
|
||||
import {
|
||||
deletePreset,
|
||||
getPreset,
|
||||
importAndSavePreset,
|
||||
listPresets,
|
||||
} from "../preset/store.js";
|
||||
import {
|
||||
createLlmForPreset,
|
||||
hasRealLlmConfig,
|
||||
} from "../runtime/llm-factory.js";
|
||||
import { sessionManager } from "./session-manager.js";
|
||||
|
||||
async function readBody(req: IncomingMessage): Promise<string> {
|
||||
@@ -226,6 +231,52 @@ export async function handleSettingsApi(
|
||||
return true;
|
||||
}
|
||||
|
||||
const presetProbeMatch = pathname.match(/^\/api\/presets\/([^/]+)\/probe$/);
|
||||
if (presetProbeMatch && req.method === "POST") {
|
||||
const id = decodeURIComponent(presetProbeMatch[1]);
|
||||
const preset = getPreset(id);
|
||||
if (!preset) {
|
||||
json(res, 404, { error: "预设不存在" });
|
||||
return true;
|
||||
}
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
message?: string;
|
||||
loreBefore?: string;
|
||||
history?: string;
|
||||
loreAfter?: string;
|
||||
postTurn?: string;
|
||||
complete?: boolean;
|
||||
};
|
||||
try {
|
||||
const wantComplete = body.complete !== false;
|
||||
const canComplete = wantComplete && hasRealLlmConfig();
|
||||
const llm = canComplete ? createLlmForPreset(preset) : null;
|
||||
const result = await runPresetProbe({
|
||||
preset,
|
||||
input: {
|
||||
message: body.message ?? "",
|
||||
loreBefore: body.loreBefore,
|
||||
history: body.history,
|
||||
loreAfter: body.loreAfter,
|
||||
postTurn: body.postTurn,
|
||||
},
|
||||
complete: llm
|
||||
? (messages) =>
|
||||
llm.complete(messages, { caller: "preset-probe" })
|
||||
: undefined,
|
||||
});
|
||||
if (!llm && wantComplete) {
|
||||
result.skipReason = "未配置 API Key,只返回拼装后的上下文";
|
||||
}
|
||||
json(res, 200, result);
|
||||
} catch (err) {
|
||||
json(res, 400, {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const presetEntriesMatch = pathname.match(
|
||||
/^\/api\/presets\/([^/]+)\/entries$/,
|
||||
);
|
||||
|
||||
@@ -177,7 +177,7 @@ function buildWorldSimulatorCatalog(
|
||||
id: "declare-ready",
|
||||
stage: "design",
|
||||
label: "实例就绪",
|
||||
purpose: "Worker 集已 accept 即可进游玩;若声明了开局,建议先验收开场白(非强制锁定)。",
|
||||
purpose: "Worker 集已 accept 即可进游玩;若走了开场白终节点,选定后会自动保存定稿。",
|
||||
status: hasAcceptedWorkerSet(session) ? "done" : "pending",
|
||||
});
|
||||
|
||||
|
||||
@@ -444,9 +444,11 @@ const server = createServer(async (req, res) => {
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
action?: string;
|
||||
stepParams?: Record<string, unknown>;
|
||||
openingIndex?: number;
|
||||
};
|
||||
let view;
|
||||
switch (body.action) {
|
||||
try {
|
||||
switch (body.action) {
|
||||
case "approve":
|
||||
view = await sessionManager.approve(
|
||||
sessionId,
|
||||
@@ -459,7 +461,12 @@ const server = createServer(async (req, res) => {
|
||||
view = await sessionManager.confirmIntake(sessionId);
|
||||
break;
|
||||
case "accept":
|
||||
view = await sessionManager.accept(sessionId);
|
||||
view = await sessionManager.accept(
|
||||
sessionId,
|
||||
typeof body.openingIndex === "number"
|
||||
? { openingIndex: body.openingIndex }
|
||||
: undefined,
|
||||
);
|
||||
break;
|
||||
case "skip_questions":
|
||||
view = await sessionManager.skipQuestions(sessionId);
|
||||
@@ -473,10 +480,19 @@ const server = createServer(async (req, res) => {
|
||||
case "finish":
|
||||
view = await sessionManager.finish(sessionId);
|
||||
break;
|
||||
case "retry_run":
|
||||
view = await sessionManager.abortAndRetry(sessionId);
|
||||
break;
|
||||
default:
|
||||
json(res, 400, { error: "未知 action" });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
json(res, 400, {
|
||||
error: err instanceof Error ? err.message : "操作失败",
|
||||
});
|
||||
return;
|
||||
}
|
||||
json(res, 200, view);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,12 +163,22 @@ export function projectFragmentContent(
|
||||
const frag = parseContextFragment(raw);
|
||||
const mode = projection.trim().toLowerCase();
|
||||
if (!frag) {
|
||||
if (mode === "summary" && raw.length > 800) return `${raw.slice(0, 800)}…`;
|
||||
if ((mode === "summary" || mode === "brief" || mode === "index") && raw.length > 800) {
|
||||
return `${raw.slice(0, 800)}…`;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
if (mode === "summary" || mode === "brief") {
|
||||
return frag.brief || raw;
|
||||
}
|
||||
if (mode === "index") {
|
||||
const lines: string[] = [];
|
||||
if (frag.技能) lines.push(`技能:${frag.技能}`);
|
||||
if (frag.brief) lines.push(frag.brief);
|
||||
if (frag.mount.length) lines.push(`挂载:${frag.mount.join("、")}`);
|
||||
if (frag.稳变) lines.push(`稳变:${frag.稳变}`);
|
||||
return lines.join("\n") || frag.brief || raw;
|
||||
}
|
||||
if (mode === "fields") {
|
||||
if (frag.正文 && typeof frag.正文 === "object" && !Array.isArray(frag.正文)) {
|
||||
const keys = Object.keys(frag.正文 as object);
|
||||
@@ -237,9 +247,11 @@ export function contextFragmentToView(raw: unknown): ContextFragmentView {
|
||||
const probe = rawDoc?.追问;
|
||||
if (probe && typeof probe === "object" && !Array.isArray(probe)) {
|
||||
const row = probe as Record<string, unknown>;
|
||||
const lines: string[] = [];
|
||||
if (typeof row.导语 === "string" && row.导语.trim()) lines.push(row.导语.trim());
|
||||
const qs = Array.isArray(row.题目) ? row.题目 : [];
|
||||
const lines: string[] = [];
|
||||
if (qs.length && typeof row.导语 === "string" && row.导语.trim()) {
|
||||
lines.push(row.导语.trim());
|
||||
}
|
||||
qs.forEach((q, i) => {
|
||||
if (!q || typeof q !== "object") return;
|
||||
const r = q as Record<string, unknown>;
|
||||
@@ -282,7 +294,9 @@ export function extractFragmentAskSidecar(raw: unknown): {
|
||||
|
||||
const looksFragment =
|
||||
row.schema === CONTEXT_FRAGMENT_SCHEMA ||
|
||||
(typeof row.brief === "string" && ("正文" in row || "body" in row));
|
||||
typeof row.技能 === "string" ||
|
||||
(typeof row.brief === "string" &&
|
||||
("正文" in row || "body" in row || "追问" in row));
|
||||
if (!looksFragment) return { questions: [], assessment: "" };
|
||||
|
||||
const assessmentParts: string[] = [];
|
||||
@@ -358,7 +372,12 @@ export function extractFragmentAskSidecar(raw: unknown): {
|
||||
}
|
||||
|
||||
const questions = normalizeQuestions(rawQs);
|
||||
const assessment = [lead, ...assessmentParts].filter(Boolean).join("\n");
|
||||
const assessment = [
|
||||
questions.length ? lead : "",
|
||||
...assessmentParts,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return { questions, assessment };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
import type { ContextSegmentDef } from "./context-segments.js";
|
||||
import { extractJsonObjectText } from "./worker-set-parse.js";
|
||||
import {
|
||||
DEFAULT_ON_DEMAND_REFS,
|
||||
DEFAULT_PLAY_SLOT_REFS,
|
||||
defaultPlaySlots,
|
||||
parsePlaySlots,
|
||||
playSlotIdForRef,
|
||||
PLAY_SLOT_META,
|
||||
PLAY_SLOTS_WITHOUT_DIALOGUE_HISTORY,
|
||||
resolvePlayAgentId,
|
||||
type PlaySlotsConfig,
|
||||
} from "./play-slots.js";
|
||||
import {
|
||||
@@ -87,20 +92,53 @@ function parseInsert(raw: unknown): ContextOrderInsert | undefined {
|
||||
function parseSlot(raw: unknown): ContextOrderSlot | undefined {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
||||
const row = raw as Record<string, unknown>;
|
||||
const ref = asString(row.ref) ?? asString(row.id);
|
||||
if (!ref) return undefined;
|
||||
const rawRef = asString(row.ref) ?? asString(row.id);
|
||||
if (!rawRef) return undefined;
|
||||
const slotId = resolvePlayAgentId(rawRef);
|
||||
const ref = slotId
|
||||
? slotId === "chance"
|
||||
? DEFAULT_ON_DEMAND_REFS.chance
|
||||
: DEFAULT_PLAY_SLOT_REFS[slotId]
|
||||
: rawRef;
|
||||
const insertsRaw = Array.isArray(row.inserts) ? row.inserts : [];
|
||||
const inserts = insertsRaw
|
||||
.map(parseInsert)
|
||||
.filter((x): x is ContextOrderInsert => Boolean(x))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
const label =
|
||||
asString(row.label) ??
|
||||
asString(row.name) ??
|
||||
(slotId && slotId !== "chance" ? PLAY_SLOT_META[slotId].label : undefined);
|
||||
return {
|
||||
ref,
|
||||
label: asString(row.label) ?? asString(row.name),
|
||||
label,
|
||||
inserts,
|
||||
};
|
||||
}
|
||||
|
||||
function agentEnabledFlag(row: Record<string, unknown>): boolean {
|
||||
if (typeof row.enabled === "boolean") return row.enabled;
|
||||
if (row.enabled === "false" || row.enabled === 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function playSlotsFromAgents(
|
||||
rows: Record<string, unknown>[],
|
||||
): PlaySlotsConfig {
|
||||
const base = defaultPlaySlots();
|
||||
for (const row of rows) {
|
||||
const rawId = asString(row.id) ?? asString(row.ref) ?? asString(row.label);
|
||||
if (!rawId) continue;
|
||||
const slotId = resolvePlayAgentId(rawId);
|
||||
if (!slotId) continue;
|
||||
const on = agentEnabledFlag(row);
|
||||
if (slotId === "chance") base.chance = on;
|
||||
else if (slotId === "auditor") base.auditor = on;
|
||||
else base[slotId] = on;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined {
|
||||
let row: Record<string, unknown> | null = null;
|
||||
if (typeof raw === "string") {
|
||||
@@ -118,16 +156,32 @@ export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined {
|
||||
}
|
||||
if (!row) return undefined;
|
||||
|
||||
const slotsRaw = Array.isArray(row.slots) ? row.slots : [];
|
||||
const slots = slotsRaw
|
||||
const agentsRaw = Array.isArray(row.agents) ? row.agents : [];
|
||||
const agentRows = agentsRaw.filter(
|
||||
(a): a is Record<string, unknown> =>
|
||||
Boolean(a) && typeof a === "object" && !Array.isArray(a),
|
||||
);
|
||||
const fromAgents = agentRows
|
||||
.filter(agentEnabledFlag)
|
||||
.map(parseSlot)
|
||||
.filter((x): x is ContextOrderSlot => Boolean(x));
|
||||
|
||||
const slotsRaw = Array.isArray(row.slots) ? row.slots : [];
|
||||
const fromSlots = slotsRaw
|
||||
.map(parseSlot)
|
||||
.filter((x): x is ContextOrderSlot => Boolean(x));
|
||||
|
||||
const slots = fromAgents.length ? fromAgents : fromSlots;
|
||||
if (!slots.length && row.schema !== CONTEXT_ORDER_SCHEMA) return undefined;
|
||||
|
||||
const play_slots = fromAgents.length
|
||||
? playSlotsFromAgents(agentRows)
|
||||
: parsePlaySlots(row.play_slots ?? row.playSlots);
|
||||
|
||||
return {
|
||||
schema: CONTEXT_ORDER_SCHEMA,
|
||||
brief: asString(row.brief),
|
||||
play_slots: parsePlaySlots(row.play_slots ?? row.playSlots),
|
||||
play_slots,
|
||||
slots,
|
||||
};
|
||||
}
|
||||
|
||||
+159
-39
@@ -89,17 +89,40 @@ function readTagContent(
|
||||
return items[items.length - 1]?.content?.trim() ?? "";
|
||||
}
|
||||
|
||||
/** 单段投影正文(不含 label)。供世界书 marker 填洞复用。 */
|
||||
export function renderContextSegmentBody(
|
||||
segment: ContextSegmentDef,
|
||||
inputs: Record<string, string>,
|
||||
blackboard: Blackboard,
|
||||
inputMerge: BlackboardInputMerge,
|
||||
skipExact?: Set<string>,
|
||||
skipTags?: Set<string>,
|
||||
): string {
|
||||
return formatSegmentBody(
|
||||
segment,
|
||||
inputs,
|
||||
blackboard,
|
||||
inputMerge,
|
||||
skipExact,
|
||||
skipTags,
|
||||
);
|
||||
}
|
||||
|
||||
function formatSegmentBody(
|
||||
segment: ContextSegmentDef,
|
||||
inputs: Record<string, string>,
|
||||
blackboard: Blackboard,
|
||||
inputMerge: BlackboardInputMerge,
|
||||
skipExact?: Set<string>,
|
||||
skipTags?: Set<string>,
|
||||
): string {
|
||||
if (segment.inline?.trim()) {
|
||||
return applyProjection(segment.inline.trim(), segment.projection);
|
||||
}
|
||||
const parts: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const tag of segment.tags) {
|
||||
if (skipTags?.has(tag)) continue;
|
||||
let content: string;
|
||||
if (isDialogueHistoryRef(tag)) {
|
||||
content = resolveDialogueHistory(inputs, blackboard, inputMerge);
|
||||
@@ -115,8 +138,22 @@ function formatSegmentBody(
|
||||
content = applyPolicy(content, segment.policy);
|
||||
content = applyProjection(content, segment.projection);
|
||||
}
|
||||
if (!content.trim()) continue;
|
||||
parts.push(content.trim());
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) continue;
|
||||
if (skipExact?.has(trimmed)) continue;
|
||||
if (seen.has(trimmed)) continue;
|
||||
if (
|
||||
parts.some(
|
||||
(p) =>
|
||||
p === trimmed ||
|
||||
p.includes(`\n\n${trimmed}`) ||
|
||||
p.startsWith(`${trimmed}\n\n`),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
parts.push(trimmed);
|
||||
}
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
@@ -141,6 +178,61 @@ function applyProjection(content: string, projection?: string): string {
|
||||
return projectFragmentContent(content, projection);
|
||||
}
|
||||
|
||||
const USER_REQUIREMENT_TAGS = new Set([
|
||||
"用户.需求",
|
||||
"book.brief",
|
||||
"用户.最新输入",
|
||||
"用户.worker答复",
|
||||
"用户.修订说明",
|
||||
]);
|
||||
|
||||
const CREATION_REVIEW_WORKERS = new Set([
|
||||
"design-step",
|
||||
"design-flow",
|
||||
"opening-generator",
|
||||
]);
|
||||
|
||||
function alreadyCovered(parts: string[], next: string): boolean {
|
||||
const trimmed = next.trim();
|
||||
if (!trimmed) return true;
|
||||
return parts.some(
|
||||
(p) =>
|
||||
p === trimmed ||
|
||||
p.includes(`\n\n${trimmed}`) ||
|
||||
p.startsWith(`${trimmed}\n\n`),
|
||||
);
|
||||
}
|
||||
|
||||
function pushUnique(parts: string[], next: string): void {
|
||||
const trimmed = next.trim();
|
||||
if (!trimmed || alreadyCovered(parts, trimmed)) return;
|
||||
parts.push(trimmed);
|
||||
}
|
||||
|
||||
/** 创作步:用户要求按时间排;冲突只看较新,旧条未点名推翻则仍有效。 */
|
||||
function collectUserRequirementBlock(params: {
|
||||
inputs: Record<string, string>;
|
||||
blackboard: Blackboard;
|
||||
inputMerge: BlackboardInputMerge;
|
||||
}): string {
|
||||
const { inputs, blackboard, inputMerge } = params;
|
||||
const ordered = [
|
||||
readTagContent("用户.需求", inputs, blackboard, inputMerge),
|
||||
readTagContent("book.brief", inputs, blackboard, inputMerge),
|
||||
readTagContent("用户.worker答复", inputs, blackboard, inputMerge),
|
||||
readTagContent("用户.最新输入", inputs, blackboard, inputMerge),
|
||||
readTagContent("用户.修订说明", inputs, blackboard, inputMerge),
|
||||
];
|
||||
const parts: string[] = [];
|
||||
for (const item of ordered) pushUnique(parts, item);
|
||||
if (!parts.length) return "";
|
||||
return [
|
||||
"## 【用户要求】按先后排列。冲突时以较新的为准;未被较新要求改写的旧条仍有效。",
|
||||
"",
|
||||
...parts,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 contextSegments 拼装 user 侧上下文。
|
||||
* 无 segments 时回退为 JSON inputs(兼容旧 skill)。
|
||||
@@ -178,10 +270,49 @@ export function assembleWorkerContext(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const revisionNote = readTagContent(
|
||||
"用户.修订说明",
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
const inheritExisting = segments.some((s) => s.id === "inherit-existing");
|
||||
const splitUserReq = CREATION_REVIEW_WORKERS.has(params.workerId);
|
||||
|
||||
const draftBlocks: string[] = [];
|
||||
for (const tag of params.outputTags) {
|
||||
const alreadyInSegments = segments.some((s) => s.tags.includes(tag));
|
||||
if (alreadyInSegments) continue;
|
||||
const draft = readTagContent(
|
||||
tag,
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
if (!draft) continue;
|
||||
draftBlocks.push(`### \`${tag}\`\n\n${draft}`);
|
||||
}
|
||||
|
||||
// 只有本步真有底稿(或回头修改)才算修订;上一步残留的修订说明仍作为用户要求保留
|
||||
const inheritMode = inheritExisting || (Boolean(revisionNote) && draftBlocks.length > 0);
|
||||
|
||||
const skipTags = new Set<string>();
|
||||
if (splitUserReq) {
|
||||
for (const tag of USER_REQUIREMENT_TAGS) skipTags.add(tag);
|
||||
}
|
||||
|
||||
const blocks: string[] = [];
|
||||
|
||||
const render = (seg: ContextSegmentDef) => {
|
||||
const body = formatSegmentBody(seg, params.inputs, params.blackboard, inputMerge);
|
||||
if (inheritMode && seg.id === "module-opening") return;
|
||||
const body = formatSegmentBody(
|
||||
seg,
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
undefined,
|
||||
skipTags,
|
||||
);
|
||||
if (!body) return;
|
||||
if (seg.label) {
|
||||
blocks.push(`${seg.label}\n\n${body}`);
|
||||
@@ -193,6 +324,15 @@ export function assembleWorkerContext(params: {
|
||||
// 严格按 segments 数组顺序(投影排序表顺序);不再按 static/dynamic 重排
|
||||
for (const seg of segments) render(seg);
|
||||
|
||||
if (splitUserReq) {
|
||||
const reqBlock = collectUserRequirementBlock({
|
||||
inputs: params.inputs,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
});
|
||||
if (reqBlock) blocks.push(reqBlock);
|
||||
}
|
||||
|
||||
// 定稿摘要:若未在 segments 中声明,仍附在末尾
|
||||
const brief = params.inputs[CONTEXT_BRIEF_TAG]?.trim();
|
||||
const briefInSegments = segments.some((s) => s.tags.includes(CONTEXT_BRIEF_TAG));
|
||||
@@ -200,39 +340,17 @@ export function assembleWorkerContext(params: {
|
||||
blocks.push(`## 上下文.定稿摘要\n\n${brief}`);
|
||||
}
|
||||
|
||||
// 修订态:把本步 outputTags 上已有正文钉成「待改底稿」,禁止无故推倒重写
|
||||
const revisionNote = readTagContent(
|
||||
"用户.修订说明",
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
const draftBlocks: string[] = [];
|
||||
if (revisionNote) {
|
||||
for (const tag of params.outputTags) {
|
||||
const alreadyInSegments = segments.some((s) => s.tags.includes(tag));
|
||||
if (alreadyInSegments) continue;
|
||||
const draft = readTagContent(
|
||||
tag,
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
if (!draft) continue;
|
||||
draftBlocks.push(`### \`${tag}\`\n\n${draft}`);
|
||||
}
|
||||
if (draftBlocks.length) {
|
||||
blocks.push(
|
||||
[
|
||||
"## 【待改底稿】",
|
||||
"",
|
||||
"下列为**当前已有产物**。按「用户.修订说明」以及「用户.worker答复」中的追问作答(选项与补充)在其上修改,并写回同名 outputTags。",
|
||||
"节点方法(task / principles / probe / output)仍须遵守。保留未要求改动的结构与结论;禁止无故整份重写。",
|
||||
"",
|
||||
...draftBlocks,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
if (inheritMode && draftBlocks.length) {
|
||||
blocks.push(
|
||||
[
|
||||
"## 【待改底稿】",
|
||||
"",
|
||||
"下列为**当前已有产物**(只保留这一份最新稿)。在其上落实【用户要求】里较新的条目;与旧要求冲突时以较新的为准,未点名改写的旧条仍须保留在产物里。",
|
||||
"节点方法(task / principles / probe / output)仍须遵守。禁止无视较新要求原样交回。",
|
||||
"",
|
||||
...draftBlocks,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const taskLines = [
|
||||
@@ -243,10 +361,12 @@ export function assembleWorkerContext(params: {
|
||||
`- outputTags: ${params.outputTags.map((t) => `\`${t}\``).join("、") || "(无)"}`,
|
||||
"",
|
||||
];
|
||||
if (revisionNote) {
|
||||
if (inheritMode) {
|
||||
taskLines.push(
|
||||
"**修订模式**:接着上方【待改底稿】(若有)与依赖产物,落实「用户.修订说明」以及「用户.worker答复」里的追问作答(选项与补充意见)。",
|
||||
"不要从零另起一份;不要扩大修改面。标为「只读 / 已定稿」的分区仍不可改。节点方法仍有效。",
|
||||
inheritExisting
|
||||
? "**回头修改**:接着上方【既有产物 · 继承修改】(以及【待改底稿】若有)继续改,写回同一产物。"
|
||||
: "**修订模式**:以【待改底稿】为底,落实【用户要求】;冲突时以较新的为准。",
|
||||
"不要从零另起一份。标为「只读 / 已定稿」的分区仍不可改。",
|
||||
);
|
||||
} else {
|
||||
taskLines.push(
|
||||
|
||||
+316
-63
@@ -53,9 +53,41 @@ const DEFAULT_SKILLS_ROOT = path.resolve(
|
||||
"../../skills",
|
||||
);
|
||||
|
||||
/** 步骤调用参数(编排期钉死;执行期只读) */
|
||||
/** 步骤调用参数。普通节点:编排期钉死、执行只读。〔先验产物〕:编排器规划的产物内容(可空),执行时注入为【规划产物】。 */
|
||||
export type CreationFlowStepParams = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 节点特性。缺省 = 普通执行步。后续会加更多 kind。
|
||||
* prior-artifact = 先验产物:须先定「写什么」(生成规则、具体实例);编排器可提前规划,步内再钉/修订。
|
||||
*/
|
||||
export const MODULE_NODE_KINDS = ["prior-artifact"] as const;
|
||||
export type ModuleNodeKind = (typeof MODULE_NODE_KINDS)[number];
|
||||
|
||||
export const MODULE_NODE_KIND_FLAGS: Record<ModuleNodeKind, string> = {
|
||||
"prior-artifact": "〔先验产物〕",
|
||||
};
|
||||
|
||||
export function parseModuleNodeKind(raw: unknown): ModuleNodeKind | undefined {
|
||||
if (typeof raw !== "string") return undefined;
|
||||
const key = raw.trim();
|
||||
return (MODULE_NODE_KINDS as readonly string[]).includes(key)
|
||||
? (key as ModuleNodeKind)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function isPriorArtifactModule(
|
||||
module: { kind?: ModuleNodeKind } | null | undefined,
|
||||
): boolean {
|
||||
return module?.kind === "prior-artifact";
|
||||
}
|
||||
|
||||
/**
|
||||
* 本步怎么跑:
|
||||
* - fresh(缺省)= 从零产出。〔可反复〕再编入新 id 必须用这个——彻底新建,不改旧条。
|
||||
* - revise = 回头修改:继承 `revises` 所指那步的既有产物继续改。
|
||||
*/
|
||||
export type CreationFlowStepMode = "fresh" | "revise";
|
||||
|
||||
export type CreationFlowStep = {
|
||||
/**
|
||||
* 本局步骤唯一 id(验收与 depends_on 用这个)。
|
||||
@@ -67,10 +99,15 @@ export type CreationFlowStep = {
|
||||
/** 依赖的其它步骤 id(旧稿若 name 唯一也可写 name) */
|
||||
depends_on: string[];
|
||||
/**
|
||||
* 本步调用参数。有「编排参数」声明的能力必须在进 design-step 前钉齐必填项。
|
||||
* 例:生成规则 → target;具体实例 → rule_id。
|
||||
* 本步调用参数。
|
||||
* 普通节点:有必填声明时须在进执行前钉齐。
|
||||
* 〔先验产物〕:编排器规划的产物内容(可空);执行注入【规划产物】,步内可修订。
|
||||
*/
|
||||
params?: CreationFlowStepParams;
|
||||
/** 缺省 fresh。revise = 继承旧产物修改,不是再生成一条。 */
|
||||
mode?: CreationFlowStepMode;
|
||||
/** mode=revise 时:被改的既有步骤 id(须更前、同 name) */
|
||||
revises?: string;
|
||||
};
|
||||
|
||||
export type CreationFlowStatus = "open" | "closed";
|
||||
@@ -111,14 +148,29 @@ export type ModuleCatalogEntry = {
|
||||
* 程序不硬拦;给编排与校验提示。
|
||||
*/
|
||||
repeatable?: boolean;
|
||||
/**
|
||||
* 可选:创作终节点。选定后程序收口并保存,其后不要再追加步骤。
|
||||
*/
|
||||
closer?: boolean;
|
||||
/**
|
||||
* 可选:程序步。不抛默认问题、不经「同意并开始」,提案后直接执行;
|
||||
* 产物仍走验收。适合投影排序这类只排序、几乎不问用户的收成步。
|
||||
*/
|
||||
auto?: boolean;
|
||||
/**
|
||||
* 可选:节点特性。缺省为普通执行步。
|
||||
* prior-artifact = 先验产物,须先定写什么;params 是规划产物而非拦执行的必填项。
|
||||
*/
|
||||
kind?: ModuleNodeKind;
|
||||
/**
|
||||
* 可选:默认问题(开场白)。优先用 prompt.md 的 ```opening 块;
|
||||
* catalog 写了则作覆盖。程序发出,不经 LLM。
|
||||
*/
|
||||
opening?: string;
|
||||
/**
|
||||
* 可选:编排期步骤参数声明。
|
||||
* 有 required 项时,steps[].params 必须在进执行前钉齐;勿把选型推迟到 design-step。
|
||||
* 可选:步骤参数声明。
|
||||
* 普通节点:required 须在进执行前钉齐。
|
||||
* 〔先验产物〕:规划产物字段;可提前写入 params,空则步内钉,不拦确认开干。
|
||||
*/
|
||||
params?: ModuleParamSpec[];
|
||||
/** 来自 prompt.md ```meta:何时该选用(编排选型) */
|
||||
@@ -191,15 +243,21 @@ export type CreationFlowUserView = {
|
||||
id: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
/** 同能力第几次(>1 时 UI 可标「再来」) */
|
||||
/** 同能力第几次(>1 时 UI 可标「再来」;回头修改不计入「第 N 次新建」) */
|
||||
occurrence?: number;
|
||||
/** fresh=从零新建;revise=回头修改 */
|
||||
mode?: CreationFlowStepMode;
|
||||
/** 回头修改时:被改的既有步骤 id */
|
||||
revises?: string;
|
||||
/** 目录里的短声明(有则展示) */
|
||||
declaration?: string;
|
||||
repeatable?: boolean;
|
||||
/** 本步调用参数(编排期钉死) */
|
||||
/** 本步调用参数(普通节点编排钉死;先验产物=规划内容) */
|
||||
params?: CreationFlowStepParams;
|
||||
/** 参数缺必填项时的提示(给人看) */
|
||||
/** 参数缺必填项时的提示(给人看);先验产物不拦执行,不出现此项 */
|
||||
paramsMissing?: string[];
|
||||
/** 节点特性(如先验产物) */
|
||||
kind?: ModuleNodeKind;
|
||||
/** 已执行 / 将要执行 / 未执行 */
|
||||
runState: FlowStepRunState;
|
||||
}>;
|
||||
@@ -241,11 +299,12 @@ export function normalizeStepParams(
|
||||
return Object.keys(out).length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
/** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序) */
|
||||
/** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序)。〔先验产物〕不拦执行,恒为 []。 */
|
||||
export function missingRequiredStepParams(
|
||||
step: Pick<CreationFlowStep, "params">,
|
||||
module: ModuleCatalogEntry | null | undefined,
|
||||
): string[] {
|
||||
if (isPriorArtifactModule(module)) return [];
|
||||
const specs = module?.params ?? [];
|
||||
if (specs.length === 0) return [];
|
||||
const params = step.params ?? {};
|
||||
@@ -278,14 +337,120 @@ export function formatStepParamsForPrompt(
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/** 为缺 id 的步骤补齐唯一 id;同 name 多次 → name#2、name#3… */
|
||||
const PRIOR_ARTIFACT_CONTEXT_TITLE =
|
||||
"## 【规划产物】编排器预先为本步规划的产物内容(先验;缺则步内钉;步内可修订)";
|
||||
|
||||
/** 〔先验产物〕注入块:规划可空,身份写在片段自身。 */
|
||||
export function formatPriorArtifactContext(
|
||||
params: CreationFlowStepParams | null | undefined,
|
||||
): string {
|
||||
const body = formatStepParamsForPrompt(params);
|
||||
const note =
|
||||
!params || Object.keys(params).length === 0
|
||||
? "编排器尚未规划本步具体写什么。先在本步与用户钉「写什么」,再填产物。"
|
||||
: "以上为先验规划,不是锁死合同。用户改对象或范围时,以本步最新认定为准,并重钉对应英文 id。";
|
||||
return `${PRIOR_ARTIFACT_CONTEXT_TITLE}\n\n${body}\n\n${note}`;
|
||||
}
|
||||
|
||||
const REVISE_MODE_ALIASES = new Set([
|
||||
"revise",
|
||||
"amend",
|
||||
"edit",
|
||||
"修改",
|
||||
"回头修改",
|
||||
"继承修改",
|
||||
]);
|
||||
const FRESH_MODE_ALIASES = new Set([
|
||||
"fresh",
|
||||
"new",
|
||||
"create",
|
||||
"新建",
|
||||
"再来",
|
||||
"再来一次",
|
||||
]);
|
||||
|
||||
/** 解析 DAG 步骤的 mode;无法识别则视为缺省(fresh) */
|
||||
export function parseCreationFlowStepMode(raw: unknown): CreationFlowStepMode | undefined {
|
||||
if (typeof raw !== "string") return undefined;
|
||||
const key = raw.trim().toLowerCase();
|
||||
if (!key) return undefined;
|
||||
if (REVISE_MODE_ALIASES.has(key) || REVISE_MODE_ALIASES.has(raw.trim())) {
|
||||
return "revise";
|
||||
}
|
||||
if (FRESH_MODE_ALIASES.has(key) || FRESH_MODE_ALIASES.has(raw.trim())) {
|
||||
return "fresh";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isReviseStep(
|
||||
step: Pick<CreationFlowStep, "mode"> | null | undefined,
|
||||
): boolean {
|
||||
return step?.mode === "revise";
|
||||
}
|
||||
|
||||
function parseStepRevisesRef(raw: Record<string, unknown>): string | undefined {
|
||||
for (const key of ["revises", "revise_of", "revises_id"] as const) {
|
||||
const v = raw[key];
|
||||
if (typeof v === "string" && v.trim()) return v.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type DraftCreationFlowStep = {
|
||||
id?: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
params?: CreationFlowStepParams;
|
||||
mode?: CreationFlowStepMode;
|
||||
revises?: string;
|
||||
};
|
||||
|
||||
function withStepModeFields(
|
||||
step: DraftCreationFlowStep,
|
||||
): Pick<CreationFlowStep, "mode" | "revises"> {
|
||||
return {
|
||||
...(step.mode === "revise" ? { mode: "revise" as const } : {}),
|
||||
...(step.revises ? { revises: step.revises } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 回头修改步:补 revises(最近一条同名更前步),并继承其 params(本步已写的优先)。
|
||||
*/
|
||||
export function normalizeReviseSteps(
|
||||
steps: CreationFlowStep[],
|
||||
): CreationFlowStep[] {
|
||||
return steps.map((step, i) => {
|
||||
if (step.mode !== "revise") return step;
|
||||
let revises = step.revises?.trim() || "";
|
||||
if (!revises) {
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (steps[j]?.name === step.name) {
|
||||
revises = steps[j]!.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const origin = revises
|
||||
? steps.find((s) => s.id === revises || s.name === revises)
|
||||
: undefined;
|
||||
const inherited = origin?.params;
|
||||
const params =
|
||||
inherited && Object.keys(inherited).length
|
||||
? { ...inherited, ...(step.params ?? {}) }
|
||||
: step.params;
|
||||
return {
|
||||
...step,
|
||||
...(revises ? { revises } : {}),
|
||||
...(params && Object.keys(params).length ? { params } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 为缺 id 的步骤补齐唯一 id;同 name 多次 → name#n;回头修改 → name·改 */
|
||||
export function ensureCreationFlowStepIds(
|
||||
steps: Array<{
|
||||
id?: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
params?: CreationFlowStepParams;
|
||||
}>,
|
||||
steps: Array<DraftCreationFlowStep>,
|
||||
): CreationFlowStep[] {
|
||||
const used = new Set<string>();
|
||||
const nameCount = new Map<string, number>();
|
||||
@@ -295,10 +460,20 @@ export function ensureCreationFlowStepIds(
|
||||
const name = raw.name.trim();
|
||||
const n = (nameCount.get(name) ?? 0) + 1;
|
||||
nameCount.set(name, n);
|
||||
const revise = raw.mode === "revise";
|
||||
|
||||
let id = typeof raw.id === "string" ? raw.id.trim() : "";
|
||||
if (!id) {
|
||||
id = n === 1 ? name : `${name}#${n}`;
|
||||
if (revise) {
|
||||
id = n === 1 ? `${name}·改` : `${name}·改`;
|
||||
if (used.has(id)) {
|
||||
let i = 2;
|
||||
while (used.has(`${name}·改#${i}`)) i++;
|
||||
id = `${name}·改#${i}`;
|
||||
}
|
||||
} else {
|
||||
id = n === 1 ? name : `${name}#${n}`;
|
||||
}
|
||||
}
|
||||
if (used.has(id)) {
|
||||
let i = 2;
|
||||
@@ -311,9 +486,10 @@ export function ensureCreationFlowStepIds(
|
||||
name,
|
||||
depends_on: raw.depends_on.map((d) => d.trim()).filter(Boolean),
|
||||
...(raw.params ? { params: raw.params } : {}),
|
||||
...withStepModeFields(raw),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
return normalizeReviseSteps(out);
|
||||
}
|
||||
|
||||
/** 按 id 或(唯一)name 解析步骤引用 */
|
||||
@@ -378,12 +554,7 @@ export function parseCreationFlow(raw: string | undefined | null): CreationFlow
|
||||
const stepsRaw = row.steps;
|
||||
if (!Array.isArray(stepsRaw) || stepsRaw.length === 0) return null;
|
||||
|
||||
const drafted: Array<{
|
||||
id?: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
params?: CreationFlowStepParams;
|
||||
}> = [];
|
||||
const drafted: DraftCreationFlowStep[] = [];
|
||||
for (const item of stepsRaw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
||||
const s = item as Record<string, unknown>;
|
||||
@@ -395,7 +566,17 @@ export function parseCreationFlow(raw: string | undefined | null): CreationFlow
|
||||
? depsRaw.map((d) => String(d).trim()).filter(Boolean)
|
||||
: [];
|
||||
const params = normalizeStepParams(s.params);
|
||||
drafted.push({ id, name, depends_on, ...(params ? { params } : {}) });
|
||||
const mode =
|
||||
parseCreationFlowStepMode(s.mode) ?? parseCreationFlowStepMode(s.intent);
|
||||
const revises = parseStepRevisesRef(s);
|
||||
drafted.push({
|
||||
id,
|
||||
name,
|
||||
depends_on,
|
||||
...(params ? { params } : {}),
|
||||
...(mode ? { mode } : {}),
|
||||
...(revises ? { revises } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const steps = ensureCreationFlowStepIds(drafted);
|
||||
@@ -440,6 +621,9 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
? m.opening.trim()
|
||||
: undefined;
|
||||
const repeatable = m.repeatable === true;
|
||||
const closer = m.closer === true;
|
||||
const auto = m.auto === true;
|
||||
const kind = parseModuleNodeKind(m.kind);
|
||||
const params = parseModuleParamSpecs(m.params);
|
||||
modules.push({
|
||||
id,
|
||||
@@ -447,6 +631,9 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
declaration,
|
||||
artifact,
|
||||
...(repeatable ? { repeatable: true } : {}),
|
||||
...(closer ? { closer: true } : {}),
|
||||
...(auto ? { auto: true } : {}),
|
||||
...(kind ? { kind } : {}),
|
||||
...(opening ? { opening } : {}),
|
||||
...(params ? { params } : {}),
|
||||
});
|
||||
@@ -702,7 +889,9 @@ export function mergeCreationFlowPreservingAccepted(params: {
|
||||
const changed =
|
||||
current.name !== frozen.name ||
|
||||
JSON.stringify(current.depends_on) !== JSON.stringify(frozen.depends_on) ||
|
||||
JSON.stringify(current.params ?? null) !== JSON.stringify(frozen.params ?? null);
|
||||
JSON.stringify(current.params ?? null) !== JSON.stringify(frozen.params ?? null) ||
|
||||
(current.mode ?? "fresh") !== (frozen.mode ?? "fresh") ||
|
||||
(current.revises ?? "") !== (frozen.revises ?? "");
|
||||
if (changed) {
|
||||
steps[at] = frozen;
|
||||
restored.push(frozen.id);
|
||||
@@ -795,29 +984,49 @@ export async function loadModuleCatalog(
|
||||
/** 注入 design-flow:能力名 + 选型字段(meta)+ 编排参数;非执行全文 */
|
||||
export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
|
||||
const lines = catalog.modules.map((m) => {
|
||||
const flags = m.repeatable ? "〔可反复〕" : "";
|
||||
const flags = [
|
||||
m.repeatable ? "〔可反复〕" : "",
|
||||
m.closer ? "〔收口〕" : "",
|
||||
m.auto ? "〔程序步〕" : "",
|
||||
m.kind ? MODULE_NODE_KIND_FLAGS[m.kind] : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
const parts: string[] = [`- ${m.name}${flags}:${m.declaration}`];
|
||||
if (m.when) parts.push(` 何时用:${indentMultiline(m.when, " ")}`);
|
||||
if (m.when_not) parts.push(` 何时不用:${indentMultiline(m.when_not, " ")}`);
|
||||
if (m.boundary) parts.push(` 边界:${indentMultiline(m.boundary, " ")}`);
|
||||
if (m.params && m.params.length > 0) {
|
||||
parts.push(
|
||||
` 编排参数:${m.params
|
||||
.map((p) => {
|
||||
const req = p.required ? "必填" : "可选";
|
||||
const hint = p.hint ? `,${p.hint}` : "";
|
||||
return `${p.key}(${p.label},${req}${hint})`;
|
||||
})
|
||||
.join(";")}`,
|
||||
);
|
||||
if (isPriorArtifactModule(m)) {
|
||||
parts.push(
|
||||
` 规划产物字段(可提前写入 params;空则步内钉):${m.params
|
||||
.map((p) => {
|
||||
const hint = p.hint ? `,${p.hint}` : "";
|
||||
return `${p.key}(${p.label}${hint})`;
|
||||
})
|
||||
.join(";")}`,
|
||||
);
|
||||
} else {
|
||||
parts.push(
|
||||
` 编排参数:${m.params
|
||||
.map((p) => {
|
||||
const req = p.required ? "必填" : "可选";
|
||||
const hint = p.hint ? `,${p.hint}` : "";
|
||||
return `${p.key}(${p.label},${req}${hint})`;
|
||||
})
|
||||
.join(";")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return parts.join("\n");
|
||||
});
|
||||
return [
|
||||
"【能力 · 可选工序】",
|
||||
"按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入。",
|
||||
"选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta);有「编排参数」的步骤必须在 DAG 里写齐 params,缺参时用 askUser 选项+其它,禁止空壳进执行。",
|
||||
"生成规则:改 target 时必须同步改 rule_id(英文 kebab-case)与步骤 id(生成规则·{对象});禁止只改中文对象沿用旧后台 id。",
|
||||
"按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入;标〔收口〕的是终节点:选定后结束创作并保存,排在细化终稿之后,其后不要再追加步骤;标〔程序步〕的确认编排后直接执行(不抛默认问题、不经同意并开始),产物仍验收。",
|
||||
"标〔先验产物〕的步骤(如生成规则、具体实例):须先定「写什么」。编排器可以提前规划多条并写入 params——这是正确的;执行时程序注入【规划产物】。规划可空,「生成什么」在步内钉。禁止为钉对象而 askUser 拦在确认开干前。规划或修订对象时同步改 rule_id(英文 kebab-case)与步骤 id(如 生成规则·{对象}),禁止只改中文沿用旧后台 id。",
|
||||
"其它有「编排参数」的步骤:确认开干前写齐必填 params;缺参时用 askUser 选项+其它。",
|
||||
"选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta)。",
|
||||
"两种「再来一次」必须分开:〔可反复〕再编入新 id(mode 缺省/fresh)= 彻底新建一条;回头修改已完成节点 = mode=revise + revises=原步 id,继承旧产物继续改。禁止把回头修改写成又一条 fresh。",
|
||||
"不要把能力执行全文塞进本步;执行由 design-step 注入。",
|
||||
lines.join("\n"),
|
||||
].join("\n");
|
||||
@@ -1153,8 +1362,8 @@ export function formatFlowProgressForAgent(params: {
|
||||
repeatable: boolean,
|
||||
) => {
|
||||
const flag = repeatable
|
||||
? "〔可反复:可再追加不同 id〕"
|
||||
: "〔不可反复:禁止再排一次执行〕";
|
||||
? "〔可反复:mode=fresh 再追加 = 彻底新建;要改旧条用 mode=revise〕"
|
||||
: "〔不可反复:禁止再排新建;要改已完成节点用 mode=revise 回头修改〕";
|
||||
const idBit = id ? `(id: ${id})` : "";
|
||||
doneLines.push(`- ${name}${idBit} · ${why} ${flag}`);
|
||||
doneNames.add(name);
|
||||
@@ -1196,9 +1405,12 @@ export function formatFlowProgressForAgent(params: {
|
||||
const draftLines: string[] = [];
|
||||
if (flow) {
|
||||
for (const step of flow.steps) {
|
||||
if (doneIds.has(step.id) || doneNames.has(step.name)) continue;
|
||||
if (doneIds.has(step.id) || isStepAccepted(step, accepted)) continue;
|
||||
const reviseBit = isReviseStep(step)
|
||||
? ` · 回头修改${step.revises ? `(revises: ${step.revises})` : ""}`
|
||||
: "";
|
||||
draftLines.push(
|
||||
`- ${step.name}(id: ${step.id})· 已在草案,保留原 id;不要当作新规划再写一遍`,
|
||||
`- ${step.name}(id: ${step.id})${reviseBit} · 已在草案,保留原 id;不要当作新规划再写一遍`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1210,9 +1422,9 @@ export function formatFlowProgressForAgent(params: {
|
||||
const lines = [
|
||||
"【流程进度】(程序钉死,必须遵守)",
|
||||
"你的任务是追加缺口,不是从头规划。",
|
||||
"已完成的非反复技能:只保留原 id 作 depends_on 锚点,禁止再作为将要执行的一步。",
|
||||
"草案里已有的步骤:原样保留,不要重排一遍开局。",
|
||||
"只有能力目录标了〔可反复〕的(如生成规则、具体实例)才允许再编入新 id。",
|
||||
"已完成的非反复技能:禁止再排一次「新建」(mode=fresh)。用户要改已完成节点 → 追加新 id,mode=revise,revises=原步 id;程序会继承既有产物。",
|
||||
"〔可反复〕再编入新 id 且 mode 缺省/fresh = 彻底新建一条(新对象/新批次),不是改旧的。要改已有那一条 → 同样用 mode=revise。",
|
||||
"草案里已有的步骤:原样保留,不要重排一遍开局。回头修改步插在最近已验收步之后、尚未执行的步之前。",
|
||||
];
|
||||
if (doneLines.length) {
|
||||
lines.push("", "已完成:", ...doneLines);
|
||||
@@ -1258,10 +1470,7 @@ export function validateCreationFlow(
|
||||
const allowed = catalog
|
||||
? new Set(catalog.modules.map((m) => m.name))
|
||||
: null;
|
||||
const nameCount = new Map<string, number>();
|
||||
for (const step of flow.steps) {
|
||||
nameCount.set(step.name, (nameCount.get(step.name) ?? 0) + 1);
|
||||
}
|
||||
const reportedFreshDup = new Set<string>();
|
||||
|
||||
for (let i = 0; i < flow.steps.length; i++) {
|
||||
const step = flow.steps[i]!;
|
||||
@@ -1275,14 +1484,44 @@ export function validateCreationFlow(
|
||||
}
|
||||
|
||||
const mod = catalog?.modules.find((m) => m.name === step.name);
|
||||
if (
|
||||
if (isReviseStep(step)) {
|
||||
const ref = step.revises?.trim() ?? "";
|
||||
if (!ref) {
|
||||
errors.push(
|
||||
`「${step.id}」是回头修改,但未标明 revises(应指向更前的同名步骤 id)`,
|
||||
);
|
||||
} else {
|
||||
const origin = findStepByRef(flow, ref);
|
||||
if (!origin) {
|
||||
errors.push(
|
||||
`「${step.id}」回头修改「${ref}」,但流程中没有该步骤`,
|
||||
);
|
||||
} else if (origin.id === step.id) {
|
||||
errors.push(`「${step.id}」不能回头修改自己`);
|
||||
} else if (origin.name !== step.name) {
|
||||
errors.push(
|
||||
`「${step.id}」回头修改「${origin.id}」,但能力名不同(${step.name} ≠ ${origin.name})`,
|
||||
);
|
||||
} else {
|
||||
const originIndex = flow.steps.findIndex((s) => s.id === origin.id);
|
||||
if (originIndex >= i) {
|
||||
errors.push(
|
||||
`「${step.id}」回头修改「${origin.id}」,但「${origin.id}」未排在其前面`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
catalog &&
|
||||
(nameCount.get(step.name) ?? 0) > 1 &&
|
||||
mod &&
|
||||
mod.repeatable !== true
|
||||
mod.repeatable !== true &&
|
||||
!reportedFreshDup.has(step.name) &&
|
||||
flow.steps.filter((s) => s.name === step.name && !isReviseStep(s)).length >
|
||||
1
|
||||
) {
|
||||
reportedFreshDup.add(step.name);
|
||||
errors.push(
|
||||
`「${step.name}」出现多次,但目录未标 repeatable(仅可反复能力可同名多次)`,
|
||||
`「${step.name}」出现多次新建,但目录未标 repeatable(非反复技能要改旧稿请用 mode=revise)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1306,7 +1545,7 @@ export function validateCreationFlow(
|
||||
return { ok: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/** 进 design-step 前:本步必填编排参数是否已齐 */
|
||||
/** 进 design-step 前:本步必填编排参数是否已齐。〔先验产物〕不拦。 */
|
||||
export function validateStepReadyToRun(
|
||||
step: CreationFlowStep,
|
||||
catalog: ModuleCatalog | null,
|
||||
@@ -1362,14 +1601,19 @@ export function formatCreationFlowForUser(
|
||||
const decl = new Map(
|
||||
(catalog?.modules ?? []).map((m) => [m.name, m] as const),
|
||||
);
|
||||
const seenName = new Map<string, number>();
|
||||
const seenFreshName = new Map<string, number>();
|
||||
const next = nextPendingStep(flow, acceptedStepIds);
|
||||
return {
|
||||
brief: flow.brief,
|
||||
status: flow.status,
|
||||
steps: flow.steps.map((s, i) => {
|
||||
const n = (seenName.get(s.name) ?? 0) + 1;
|
||||
seenName.set(s.name, n);
|
||||
const revise = isReviseStep(s);
|
||||
let occurrence: number | undefined;
|
||||
if (!revise) {
|
||||
const n = (seenFreshName.get(s.name) ?? 0) + 1;
|
||||
seenFreshName.set(s.name, n);
|
||||
occurrence = n;
|
||||
}
|
||||
const mod = decl.get(s.name);
|
||||
const paramsMissing = missingRequiredStepParams(s, mod);
|
||||
const runState: FlowStepRunState = isStepAccepted(s, acceptedStepIds)
|
||||
@@ -1382,9 +1626,12 @@ export function formatCreationFlowForUser(
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
depends_on: s.depends_on,
|
||||
occurrence: n,
|
||||
occurrence,
|
||||
...(revise ? { mode: "revise" as const } : {}),
|
||||
...(s.revises ? { revises: s.revises } : {}),
|
||||
declaration: mod?.declaration,
|
||||
repeatable: mod?.repeatable,
|
||||
...(mod?.kind ? { kind: mod.kind } : {}),
|
||||
runState,
|
||||
...(s.params ? { params: s.params } : {}),
|
||||
...(paramsMissing.length > 0 ? { paramsMissing } : {}),
|
||||
@@ -1443,6 +1690,7 @@ export function nextPendingStep(
|
||||
acceptedStepIds: readonly string[],
|
||||
): CreationFlowStep | null {
|
||||
if (!flow?.steps.length) return null;
|
||||
const ready: CreationFlowStep[] = [];
|
||||
for (const step of flow.steps) {
|
||||
if (isStepAccepted(step, acceptedStepIds)) continue;
|
||||
const depsOk = step.depends_on.every((dep) => {
|
||||
@@ -1450,9 +1698,9 @@ export function nextPendingStep(
|
||||
if (!depStep) return false;
|
||||
return isStepAccepted(depStep, acceptedStepIds);
|
||||
});
|
||||
if (depsOk) return step;
|
||||
if (depsOk) ready.push(step);
|
||||
}
|
||||
return null;
|
||||
return ready.find((s) => isReviseStep(s)) ?? ready[0] ?? null;
|
||||
}
|
||||
|
||||
/** 当前已列出的步骤是否都已验收(不管 status) */
|
||||
@@ -1532,8 +1780,10 @@ export type DesignStepBinding = {
|
||||
module: ModuleCatalogEntry;
|
||||
depTags: string[];
|
||||
modulePrompt: string;
|
||||
/** 程序开场白;无则本步直接调 LLM */
|
||||
/** 程序开场白;无则本步直接调 LLM。回头修改不抛 opening。 */
|
||||
opening: string | null;
|
||||
/** 回头修改时注入的既有产物 tag */
|
||||
inheritTag: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1570,7 +1820,9 @@ export async function resolveDesignStepBinding(params: {
|
||||
`# ${module.name}\n\n(模块 prompt.md 缺失,请补充 skills/.../modules/${module.id}/prompt.md)`;
|
||||
|
||||
const opening =
|
||||
module.opening?.trim() || extractModuleOpening(modulePromptRaw) || null;
|
||||
isReviseStep(step) || module.auto
|
||||
? null
|
||||
: module.opening?.trim() || extractModuleOpening(modulePromptRaw) || null;
|
||||
|
||||
return {
|
||||
step,
|
||||
@@ -1578,5 +1830,6 @@ export async function resolveDesignStepBinding(params: {
|
||||
depTags: dependencyArtifactTags(step, catalog, flow),
|
||||
modulePrompt: formatModulePromptForLlm(modulePromptRaw),
|
||||
opening,
|
||||
inheritTag: isReviseStep(step) ? module.artifact : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
slotOrderForRef,
|
||||
} from "./context-order.js";
|
||||
import type { ContextSegmentDef } from "./context-segments.js";
|
||||
import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js";
|
||||
|
||||
type WorkerTemplateDoc = {
|
||||
id?: string;
|
||||
@@ -44,8 +45,9 @@ type WorkerTemplateDoc = {
|
||||
/**
|
||||
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
|
||||
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
|
||||
* - 游玩回合:一律 no_confirmation(一句输入连跑,不穿插验收)
|
||||
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
|
||||
* - 未声明 acceptance:design 生命周期默认确认;play 缺省按 review(稳妥)
|
||||
* - 未声明 acceptance:创作默认确认
|
||||
*/
|
||||
export function resolveAcceptanceModeForWorker(params: {
|
||||
session: RuntimeSession;
|
||||
@@ -56,6 +58,9 @@ export function resolveAcceptanceModeForWorker(params: {
|
||||
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
|
||||
return "user_confirmed";
|
||||
}
|
||||
if (inferLifecycleStage(params.session) === "play") {
|
||||
return "no_confirmation";
|
||||
}
|
||||
|
||||
const decl = buildInstanceWorkerDeclaration(
|
||||
params.session,
|
||||
@@ -66,7 +71,6 @@ export function resolveAcceptanceModeForWorker(params: {
|
||||
if (entry?.acceptance === "continue") return "no_confirmation";
|
||||
if (entry?.acceptance === "review") return "user_confirmed";
|
||||
|
||||
// 未写明:创作阶段默认验收;游玩缺省也验收(避免静默连跑)
|
||||
return "user_confirmed";
|
||||
}
|
||||
|
||||
@@ -210,6 +214,11 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
params.template?.duty?.trim() ||
|
||||
`执行 ${id}`;
|
||||
const excerpt = params.template?.prompt_excerpt?.trim() || "";
|
||||
const visibleBodyRule =
|
||||
id === "narrator" || id === "round-present"
|
||||
? PLAY_VISIBLE_BODY_INSTRUCTION
|
||||
: "";
|
||||
const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n");
|
||||
const presentation = params.entry.presentation
|
||||
? JSON.stringify(params.entry.presentation, null, 2)
|
||||
: "";
|
||||
@@ -217,7 +226,7 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
const premises = (params.workerSet?.core_premises ?? []).filter(Boolean);
|
||||
const residentSection = formatResidentPromptSection(resident, id);
|
||||
|
||||
const personaText = [duty, excerpt].filter(Boolean).join("\n\n");
|
||||
const personaText = [duty, excerptWithRule].filter(Boolean).join("\n\n");
|
||||
const orderDoc = parseContextOrder(params.workerSet?.context_order);
|
||||
const orderSlot = slotOrderForRef(orderDoc, id);
|
||||
let contextSegments: ContextSegmentDef[] | undefined;
|
||||
@@ -277,7 +286,7 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
"",
|
||||
duty,
|
||||
"",
|
||||
excerpt ? `## 写法要点\n\n${excerpt}` : "",
|
||||
excerptWithRule ? `## 写法要点\n\n${excerptWithRule}` : "",
|
||||
presentation ? `## presentation(实例)\n\n\`\`\`json\n${presentation}\n\`\`\`` : "",
|
||||
narrative ? `## 叙事指南\n\n${narrative}` : "",
|
||||
premises.length
|
||||
|
||||
+40
-6
@@ -516,7 +516,9 @@ export async function loadWorkerSkillWithContext(
|
||||
resolveDesignStepBinding,
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_MODULE_OPENING_TAG,
|
||||
formatPriorArtifactContext,
|
||||
formatStepParamsForPrompt,
|
||||
isPriorArtifactModule,
|
||||
} = await import("./creation-flow.js");
|
||||
const binding = await resolveDesignStepBinding({
|
||||
skillPackRoot: skill.skillPackRoot,
|
||||
@@ -526,11 +528,27 @@ export async function loadWorkerSkillWithContext(
|
||||
skillsRoot,
|
||||
});
|
||||
if (binding) {
|
||||
const inheritTag = binding.inheritTag?.trim() || "";
|
||||
const inheritNote = inheritTag
|
||||
? `\n\n【回头修改】本步继承既有产物继续改,不是从零再生成。程序已注入【既有产物 · 继承修改】。保留未点名要改的部分,写回同一产物。禁止推倒重写,禁止再走一遍开场询问。`
|
||||
: "";
|
||||
const openingNote = binding.opening
|
||||
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
|
||||
: "";
|
||||
const paramsBlock = `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
||||
modulePromptBlock = `${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}`;
|
||||
const paramsBlock = isPriorArtifactModule(binding.module)
|
||||
? formatPriorArtifactContext(binding.step.params)
|
||||
: `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
||||
let rosterBlock = "";
|
||||
const depProjection =
|
||||
binding.module.id === "context-order" ? "index" : undefined;
|
||||
if (binding.module.id === "context-order") {
|
||||
const { formatPlayAgentRosterForPrompt } = await import("./play-slots.js");
|
||||
rosterBlock = `${formatPlayAgentRosterForPrompt()}\n\n`;
|
||||
}
|
||||
modulePromptBlock = `${rosterBlock}${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}${inheritNote}`;
|
||||
const depTags = inheritTag
|
||||
? binding.depTags.filter((tag) => tag !== inheritTag)
|
||||
: binding.depTags;
|
||||
const baseInputs = [
|
||||
"用户.需求",
|
||||
"book.brief",
|
||||
@@ -540,16 +558,29 @@ export async function loadWorkerSkillWithContext(
|
||||
"设计.创作流程",
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_MODULE_OPENING_TAG,
|
||||
...binding.depTags,
|
||||
...depTags,
|
||||
...(inheritTag ? [inheritTag] : []),
|
||||
];
|
||||
const inputTags = [...new Set(baseInputs)];
|
||||
const outputTags = [binding.module.artifact];
|
||||
const depSegments = binding.depTags.map((tag, i) => ({
|
||||
const depSegments = depTags.map((tag, i) => ({
|
||||
id: `dep-${i}`,
|
||||
tier: "static" as const,
|
||||
tags: [tag],
|
||||
label: `## 【依赖产物 · ${tag}】只读`,
|
||||
label: `## 【依赖产物 · ${tag}】只读${depProjection === "index" ? "(仅 brief / 挂载 / 稳变)" : ""}`,
|
||||
...(depProjection ? { projection: depProjection } : {}),
|
||||
}));
|
||||
const inheritSegment = inheritTag
|
||||
? [
|
||||
{
|
||||
id: "inherit-existing",
|
||||
tier: "static" as const,
|
||||
tags: [inheritTag],
|
||||
label:
|
||||
"## 【既有产物 · 继承修改】在下列正文上继续改,写回同一产物;禁止从零另起一份",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const openingSegment = binding.opening
|
||||
? [
|
||||
{
|
||||
@@ -562,13 +593,16 @@ export async function loadWorkerSkillWithContext(
|
||||
: [];
|
||||
patchedWorker = {
|
||||
...worker,
|
||||
name: `创作 · ${binding.module.name}`,
|
||||
name: inheritTag
|
||||
? `创作 · 回头修改 · ${binding.module.name}`
|
||||
: `创作 · ${binding.module.name}`,
|
||||
description: binding.module.declaration,
|
||||
inputTags,
|
||||
outputTags,
|
||||
contextSegments: [
|
||||
...(worker.contextSegments ?? []),
|
||||
...openingSegment,
|
||||
...inheritSegment,
|
||||
...depSegments,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 开场白终节点:选定后落库、关 DAG。
|
||||
* 能力 `opening-setup` 与磁盘 worker `opening-generator` 共用这条收口。
|
||||
*/
|
||||
import {
|
||||
parseTableDoc,
|
||||
stringifyTableDoc,
|
||||
type TableDoc,
|
||||
} from "../blackboard/table-cells.js";
|
||||
import {
|
||||
parseCreationFlow,
|
||||
stringifyCreationFlow,
|
||||
} from "./creation-flow.js";
|
||||
import { parseContextFragment } from "./context-fragment.js";
|
||||
|
||||
export const OPENING_SETUP_ARTIFACT_TAG = "设计.开场白与开场变量";
|
||||
export const OPENING_OUTPUT_TAG = "输出.开场白";
|
||||
export const OPENING_INITIAL_VARS_TAG = "运行.初始变量";
|
||||
export const OPENING_CURRENT_VARS_TAG = "变量.当前";
|
||||
|
||||
export const SLOT_CREATION_SEALED_BY_OPENING = "creationSealedByOpening";
|
||||
export const SLOT_OPENING_SELECTED_INDEX = "openingSelectedIndex";
|
||||
|
||||
export const CREATION_SEALED_WAITING_MESSAGE =
|
||||
"开场已选定,创作已收口并保存。可切换到「游玩」。";
|
||||
|
||||
export const INSTANCE_OPENING_SNAPSHOT_LABEL = "创作定稿(开场)";
|
||||
|
||||
export function isOpeningLockTag(tag: string): boolean {
|
||||
const t = tag.trim();
|
||||
return t === OPENING_OUTPUT_TAG || t === OPENING_INITIAL_VARS_TAG;
|
||||
}
|
||||
|
||||
export function isOpeningSealArtifact(artifact: {
|
||||
workerId?: string;
|
||||
outputTags?: readonly string[];
|
||||
}): boolean {
|
||||
if ((artifact.workerId ?? "").trim() === "opening-generator") return true;
|
||||
return (artifact.outputTags ?? []).includes(OPENING_SETUP_ARTIFACT_TAG);
|
||||
}
|
||||
|
||||
export type OpeningVariable = {
|
||||
name: string;
|
||||
value: unknown;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export type OpeningSealPayload = {
|
||||
candidates: string[];
|
||||
selectedIndex: number;
|
||||
selectedText: string;
|
||||
variables: OpeningVariable[];
|
||||
};
|
||||
|
||||
function asTrimmed(v: unknown): string | undefined {
|
||||
if (typeof v !== "string") return undefined;
|
||||
const t = v.trim();
|
||||
return t || undefined;
|
||||
}
|
||||
|
||||
function collectCandidateTexts(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const out: string[] = [];
|
||||
for (const item of raw) {
|
||||
if (typeof item === "string" && item.trim()) {
|
||||
out.push(item.trim());
|
||||
continue;
|
||||
}
|
||||
if (item && typeof item === "object" && !Array.isArray(item)) {
|
||||
const row = item as Record<string, unknown>;
|
||||
const text =
|
||||
asTrimmed(row.全文) ||
|
||||
asTrimmed(row.text) ||
|
||||
asTrimmed(row.开场白) ||
|
||||
asTrimmed(row.内容);
|
||||
if (text) out.push(text);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectVariables(raw: unknown): OpeningVariable[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const out: OpeningVariable[] = [];
|
||||
for (const item of raw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const row = item as Record<string, unknown>;
|
||||
const name =
|
||||
asTrimmed(row.名) || asTrimmed(row.name) || asTrimmed(row.key) || "";
|
||||
if (!name) continue;
|
||||
const note = asTrimmed(row.依据) || asTrimmed(row.note);
|
||||
out.push({
|
||||
name,
|
||||
value: row.值 !== undefined ? row.值 : row.value,
|
||||
...(note ? { note } : {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function bodyRecord(fragmentBody: unknown): Record<string, unknown> | null {
|
||||
if (!fragmentBody || typeof fragmentBody !== "object" || Array.isArray(fragmentBody)) {
|
||||
return null;
|
||||
}
|
||||
return fragmentBody as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** 从开场白 fragment(或裸 JSON)抽出候选与变量。 */
|
||||
export function parseOpeningSealPayload(
|
||||
raw: string | null | undefined,
|
||||
selectedIndex = 0,
|
||||
): OpeningSealPayload | null {
|
||||
if (!raw?.trim()) return null;
|
||||
const fragment = parseContextFragment(raw);
|
||||
const body = bodyRecord(fragment?.正文) ?? (() => {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
return bodyRecord(parsed) ?? bodyRecord((parsed as { 正文?: unknown })?.正文);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const primary =
|
||||
asTrimmed(body?.开场白全文) ||
|
||||
asTrimmed(body?.["输出.开场白"]) ||
|
||||
(typeof fragment?.正文 === "string" ? asTrimmed(fragment.正文) : undefined);
|
||||
|
||||
const fromList = collectCandidateTexts(body?.开场白候选);
|
||||
const candidates: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (text: string | undefined) => {
|
||||
if (!text || seen.has(text)) return;
|
||||
seen.add(text);
|
||||
candidates.push(text);
|
||||
};
|
||||
push(primary);
|
||||
for (const t of fromList) push(t);
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
const max = candidates.length - 1;
|
||||
const idx = Number.isFinite(selectedIndex)
|
||||
? Math.min(max, Math.max(0, Math.trunc(selectedIndex)))
|
||||
: 0;
|
||||
|
||||
return {
|
||||
candidates,
|
||||
selectedIndex: idx,
|
||||
selectedText: candidates[idx] ?? candidates[0]!,
|
||||
variables: collectVariables(body?.开场变量),
|
||||
};
|
||||
}
|
||||
|
||||
export function openingVariablesToTableDoc(
|
||||
variables: readonly OpeningVariable[],
|
||||
source = "worker:opening-setup",
|
||||
): TableDoc {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
rows: variables.map((v) => ({
|
||||
key: v.name,
|
||||
value: v.value,
|
||||
rev: 1,
|
||||
updatedAt: now,
|
||||
source,
|
||||
visibility: "visible" as const,
|
||||
...(v.note ? { note: v.note } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeOpeningTablePatch(
|
||||
currentRaw: string | null | undefined,
|
||||
variables: readonly OpeningVariable[],
|
||||
source = "worker:opening-setup",
|
||||
): string | null {
|
||||
if (!variables.length) return null;
|
||||
const patch = openingVariablesToTableDoc(variables, source);
|
||||
const current = parseTableDoc(currentRaw);
|
||||
if (!current) return stringifyTableDoc(patch);
|
||||
const byKey = new Map(current.rows.map((r) => [r.key, r]));
|
||||
const now = new Date().toISOString();
|
||||
for (const row of patch.rows) {
|
||||
const prev = byKey.get(row.key);
|
||||
if (prev && String(prev.source).startsWith("user")) continue;
|
||||
byKey.set(row.key, {
|
||||
...row,
|
||||
rev: (prev?.rev ?? 0) + 1,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return stringifyTableDoc({ rows: [...byKey.values()] });
|
||||
}
|
||||
|
||||
export function closeCreationFlowRaw(raw: string | null | undefined): string | null {
|
||||
const flow = parseCreationFlow(raw);
|
||||
if (!flow) return raw?.trim() ? raw : null;
|
||||
if (flow.status === "closed") return stringifyCreationFlow(flow);
|
||||
return stringifyCreationFlow({ ...flow, status: "closed" });
|
||||
}
|
||||
@@ -319,3 +319,54 @@ export function inferPlaySlotsFromWorkers(
|
||||
chance: hasChance,
|
||||
};
|
||||
}
|
||||
|
||||
const AGENT_ID_ALIASES: Record<string, PlaySlotId | OnDemandSlotId> = {
|
||||
auditor: "auditor",
|
||||
旁观维护: "auditor",
|
||||
表格管理: "auditor",
|
||||
gm: "gm",
|
||||
"world-simulator": "gm",
|
||||
主世界层: "gm",
|
||||
narrator: "narrator",
|
||||
叙事转述: "narrator",
|
||||
转述: "narrator",
|
||||
perspective: "perspective",
|
||||
"role-decide": "perspective",
|
||||
角色视角: "perspective",
|
||||
chance: "chance",
|
||||
机遇裁定: "chance",
|
||||
};
|
||||
|
||||
/** 把模型写的 id / 中文名 / worker ref 收成槽键 */
|
||||
export function resolvePlayAgentId(
|
||||
raw: string,
|
||||
): PlaySlotId | OnDemandSlotId | undefined {
|
||||
const t = raw.trim();
|
||||
return AGENT_ID_ALIASES[t] ?? AGENT_ID_ALIASES[t.toLowerCase()];
|
||||
}
|
||||
|
||||
/** 注入投影排序等晚段步骤:名册由程序钉死,模型只抄不发明。 */
|
||||
export function formatPlayAgentRosterForPrompt(): string {
|
||||
const lines = [
|
||||
"## 【本局固定执行单元 · 程序钉死】",
|
||||
"",
|
||||
"禁止增删改名,禁止发明未入白名单的槽(如画图)。先抄名册,再给每个已启用者分配上下文 inserts。",
|
||||
"",
|
||||
"每轮管线(顺序固定):",
|
||||
];
|
||||
for (const id of PLAY_SLOT_ORDER) {
|
||||
const meta = PLAY_SLOT_META[id];
|
||||
const def = id === "perspective" ? "默认关" : "默认开";
|
||||
lines.push(
|
||||
`- \`${id}\` → ref=\`${DEFAULT_PLAY_SLOT_REFS[id]}\` **${meta.label}**(${def}):${meta.purpose}`,
|
||||
);
|
||||
}
|
||||
lines.push("", "按需(不进每轮;enabled 时可空 inserts):");
|
||||
for (const id of Object.keys(DEFAULT_ON_DEMAND_REFS) as OnDemandSlotId[]) {
|
||||
const meta = ON_DEMAND_SLOT_META[id];
|
||||
lines.push(
|
||||
`- \`${id}\` → ref=\`${DEFAULT_ON_DEMAND_REFS[id]}\` **${meta.label}**(默认关):${meta.purpose}`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 游玩回合:按运行规格 playWorkerIds 连跑(旁观 → 视角? → 主世界层 → 转述)。
|
||||
* 与创作总管无关;收口后的输入走这里。
|
||||
*/
|
||||
export const SLOT_PLAY_TURN_QUEUE = "playTurnQueue";
|
||||
export const SLOT_PLAY_LAYER_ACTIVE = "playLayerActive";
|
||||
|
||||
export const PLAY_WORKING_SNAPSHOT_ID = "play-working";
|
||||
export const PLAY_WORKING_SNAPSHOT_LABEL = "当前游玩";
|
||||
|
||||
export function isPlayLayerActive(slots: Record<string, unknown> | undefined): boolean {
|
||||
return Boolean(slots?.[SLOT_PLAY_LAYER_ACTIVE]);
|
||||
}
|
||||
|
||||
export function readPlayTurnQueue(
|
||||
slots: Record<string, unknown> | undefined,
|
||||
): string[] {
|
||||
const raw = slots?.[SLOT_PLAY_TURN_QUEUE];
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw
|
||||
.filter((id): id is string => typeof id === "string" && Boolean(id.trim()))
|
||||
.map((id) => id.trim());
|
||||
}
|
||||
|
||||
export function withPlayTurnQueue(
|
||||
slots: Record<string, unknown>,
|
||||
queue: string[] | undefined,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...slots,
|
||||
[SLOT_PLAY_TURN_QUEUE]: queue?.length ? queue : undefined,
|
||||
};
|
||||
}
|
||||
@@ -132,6 +132,9 @@ function normalizeBlocks(raw: unknown): PresentBlocks {
|
||||
if (out.monitor === undefined && row.监控 !== undefined) {
|
||||
out.monitor = row.监控 as PresentBlocks["monitor"];
|
||||
}
|
||||
if (typeof out.body === "string") {
|
||||
out.body = stripPresentSourceFences(out.body);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -170,15 +173,35 @@ export function presentFromPlainText(
|
||||
return {
|
||||
schema: PRESENT_SCHEMA,
|
||||
shell,
|
||||
blocks: { body: text },
|
||||
blocks: { body: stripPresentSourceFences(text) },
|
||||
};
|
||||
}
|
||||
|
||||
/** 模型常把 tag 名写进正文开头;展示前剥掉 */
|
||||
export function stripPresentSourceFences(raw: string): string {
|
||||
let s = raw.trim();
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const next = s
|
||||
.replace(/^(#{1,6}\s*)?输出[.::]?\s*(用户展示|开场白)(?:\s*\n+|\s*$)/u, "")
|
||||
.replace(/^【\s*输出[.::]?\s*(用户展示|开场白)\s*】(?:\s*\n+|\s*$)/u, "")
|
||||
.trim();
|
||||
if (next === s) break;
|
||||
s = next;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export const PLAY_VISIBLE_BODY_INSTRUCTION = [
|
||||
"用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **500~2000 字**(按汉字计)。",
|
||||
"不要写成几句气泡短信就结束,除非用户明确要求极短。",
|
||||
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
||||
].join("\n");
|
||||
|
||||
export function parsePresentPacket(
|
||||
raw: string | undefined | null,
|
||||
fallbackShell: PresentShellId = "prose",
|
||||
): PresentPacketView {
|
||||
const text = raw?.trim() ?? "";
|
||||
const text = stripPresentSourceFences(raw?.trim() ?? "");
|
||||
if (!text) {
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||
import type { RuntimeSession } from "../types/runtime.js";
|
||||
import { isPlayLayerActive } from "./play-turn.js";
|
||||
import {
|
||||
deriveDesignStageScope,
|
||||
deriveOnDemandWorkerScope,
|
||||
@@ -45,8 +46,9 @@ export function canEnterPlay(session: RuntimeSession): boolean {
|
||||
}
|
||||
|
||||
export function inferLifecycleStage(session: RuntimeSession): LifecycleStage {
|
||||
const override = session.slots.uiLifecycleStage;
|
||||
if (override === "play" && canEnterPlay(session)) return "play";
|
||||
if (!canEnterPlay(session)) return "design";
|
||||
if (session.slots.uiLifecycleStage === "play") return "play";
|
||||
if (isPlayLayerActive(session.slots)) return "play";
|
||||
return "design";
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,13 @@ import {
|
||||
OpenAiCompatibleProvider,
|
||||
type LlmProvider,
|
||||
} from "../llm/client.js";
|
||||
import { PresetLlmProvider } from "../llm/preset-wrapper.js";
|
||||
import { resolveActivePreset } from "../preset/store.js";
|
||||
import { loadAppSettings } from "../config/settings.js";
|
||||
import {
|
||||
TokenTrackingProvider,
|
||||
type LlmTrackingContext,
|
||||
} from "../llm/token-tracker.js";
|
||||
wrapLlmForSession,
|
||||
type LlmTrackingRef,
|
||||
} from "../llm/preset-wrapper.js";
|
||||
import type { ParsedWorkerSkill, SkillWorkerLlmBindings } from "./types.js";
|
||||
|
||||
type LlmTrackingRef = { current: LlmTrackingContext };
|
||||
export type { LlmTrackingContext } from "../llm/token-tracker.js";
|
||||
|
||||
/** 按 ApiProfile.id 构建 LLM;找不到 profile 时回退 fallback */
|
||||
export function createLlmForProfileId(
|
||||
@@ -25,17 +22,10 @@ export function createLlmForProfileId(
|
||||
return fallback;
|
||||
}
|
||||
|
||||
let inner: LlmProvider = new OpenAiCompatibleProvider(
|
||||
profileToLlmConfig(profile),
|
||||
return wrapLlmForSession(
|
||||
new OpenAiCompatibleProvider(profileToLlmConfig(profile)),
|
||||
trackingRef,
|
||||
);
|
||||
const preset = resolveActivePreset(loadAppSettings().activePresetId);
|
||||
if (preset) {
|
||||
inner = new PresetLlmProvider(inner, () =>
|
||||
resolveActivePreset(loadAppSettings().activePresetId),
|
||||
);
|
||||
}
|
||||
if (!trackingRef) return inner;
|
||||
return new TokenTrackingProvider(inner, () => trackingRef.current);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,5 +132,3 @@ export function filterInputsForRolePerspective(
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export type { LlmTrackingContext };
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { GenerationParameters } from "./preset.js";
|
||||
|
||||
/** 一次 LLM 调用实际发送的上下文(全量,供右键观察) */
|
||||
export type LlmContextMessage = {
|
||||
role: string;
|
||||
@@ -12,17 +14,51 @@ export type LlmContextTrace = {
|
||||
/** 便于列表展示 */
|
||||
charCount: number;
|
||||
model?: string;
|
||||
/** 与请求一并送出的生成参数(无则未带) */
|
||||
generation?: GenerationParameters;
|
||||
};
|
||||
|
||||
/** 把即将写入请求 body.messages 的条目冻成痕迹(含 tool_calls 等附加字段) */
|
||||
export function snapshotRequestMessages(
|
||||
messages: Array<{
|
||||
role: string;
|
||||
content?: string | null;
|
||||
tool_calls?: unknown;
|
||||
tool_call_id?: string;
|
||||
}>,
|
||||
): LlmContextMessage[] {
|
||||
return messages.map((m) => {
|
||||
const hasExtras = Boolean(m.tool_calls) || Boolean(m.tool_call_id);
|
||||
if (!hasExtras) {
|
||||
return {
|
||||
role: m.role,
|
||||
content: typeof m.content === "string" ? m.content : String(m.content ?? ""),
|
||||
};
|
||||
}
|
||||
return {
|
||||
role: m.role,
|
||||
content: JSON.stringify({
|
||||
role: m.role,
|
||||
content: m.content ?? "",
|
||||
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
|
||||
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function buildContextTrace(params: {
|
||||
caller: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
messages: Array<{
|
||||
role: string;
|
||||
content?: string | null;
|
||||
tool_calls?: unknown;
|
||||
tool_call_id?: string;
|
||||
}>;
|
||||
model?: string;
|
||||
generation?: GenerationParameters;
|
||||
}): LlmContextTrace {
|
||||
const messages = params.messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: typeof m.content === "string" ? m.content : String(m.content ?? ""),
|
||||
}));
|
||||
const messages = snapshotRequestMessages(params.messages);
|
||||
const charCount = messages.reduce((n, m) => n + m.content.length, 0);
|
||||
return {
|
||||
caller: params.caller,
|
||||
@@ -30,6 +66,9 @@ export function buildContextTrace(params: {
|
||||
messages,
|
||||
charCount,
|
||||
model: params.model,
|
||||
...(params.generation && Object.keys(params.generation).length
|
||||
? { generation: params.generation }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -53,7 +53,10 @@ export type WaitingReason =
|
||||
} // worker 无产物时的阻塞提问
|
||||
| { kind: "revision"; instruction?: string } // 无意见打回时等补交;有意见则直接重跑
|
||||
| {
|
||||
/** 上一步已验收:先问「下一步想写什么」(可留空),再提案并确认开干 */
|
||||
/**
|
||||
* 旧路径:上一步验收后先问「下一步想写什么」。
|
||||
* 现已改为验收后直接提案;保留此 kind 以兼容未完成的旧会话。
|
||||
*/
|
||||
kind: "next_intent";
|
||||
afterWorkerId?: string;
|
||||
afterUnitId?: string;
|
||||
@@ -329,7 +332,13 @@ export type PhaseEffect =
|
||||
| { type: "run_programmatic_review"; artifactId: string }
|
||||
| { type: "emit_message"; message: string }
|
||||
/** 根据流程与用户下一步意向,提案下一节点并进入确认 */
|
||||
| { type: "propose_next_creation_step" };
|
||||
| { type: "propose_next_creation_step" }
|
||||
/** 选定开场白:落库、关 DAG、标记创作收口 */
|
||||
| { type: "seal_creation_opening" }
|
||||
/** 游玩:按运行规格开一轮(旁观 → 主世界层 → 转述) */
|
||||
| { type: "run_play_turn" }
|
||||
/** 游玩:跑管线里的下一个执行单元;队空则等下一条用户输入 */
|
||||
| { type: "continue_play_turn" };
|
||||
|
||||
/** applyEvent 的返回值:新会话快照 + 待处理副作用 + 可选错误 */
|
||||
export type ApplyEventResult = {
|
||||
|
||||
+129
-80
@@ -11,9 +11,16 @@ import type { ParsedWorkerSkill } from "../skills/types.js";
|
||||
import {
|
||||
extractJsonObjectText,
|
||||
isUsableWorkerSet,
|
||||
looksLikeProseNotSpec,
|
||||
parseWorkerSetYaml,
|
||||
} from "../skills/worker-set-parse.js";
|
||||
import { assembleWorkerContext } from "../skills/context-segments.js";
|
||||
import { isPlayLayerActive } from "../skills/play-turn.js";
|
||||
import { loadAppSettings } from "../config/settings.js";
|
||||
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 {
|
||||
mergeQuestionsPreferFragment,
|
||||
normalizeQuestions,
|
||||
@@ -21,11 +28,15 @@ import {
|
||||
} from "../skills/question-protocol.js";
|
||||
import {
|
||||
extractFragmentAskSidecar,
|
||||
expectsContextFragmentTag,
|
||||
isUsableContextFragment,
|
||||
looksLikeFragmentDoc,
|
||||
} from "../skills/context-fragment.js";
|
||||
import { isProgressPointerTag } from "../skills/creation-flow.js";
|
||||
import {
|
||||
CONTEXT_ORDER_TAG,
|
||||
parseContextOrder,
|
||||
serializeContextOrder,
|
||||
} from "../skills/context-order.js";
|
||||
|
||||
export type WorkerRunParams = {
|
||||
skillName: string;
|
||||
@@ -53,6 +64,25 @@ export type WorkerRunResult = {
|
||||
};
|
||||
|
||||
const WORKER_SET_OUTPUT_TAGS = new Set(["设计.worker集", "设计.worker集.草稿"]);
|
||||
const PLAY_FEED_OUTPUT_TAGS = new Set([PRESENT_TAG, "输出.开场白"]);
|
||||
|
||||
function formatWorkerPreview(
|
||||
outputs: Record<string, string>,
|
||||
summary: string,
|
||||
): string {
|
||||
const visible = Object.entries(outputs).filter(([tag]) =>
|
||||
PLAY_FEED_OUTPUT_TAGS.has(tag),
|
||||
);
|
||||
if (visible.length) {
|
||||
return visible.map(([, v]) => v).join("\n\n");
|
||||
}
|
||||
return (
|
||||
Object.entries(outputs)
|
||||
.map(([tag, v]) => `### ${tag}\n\n${v}`)
|
||||
.join("\n\n")
|
||||
.slice(0, 4000) || summary
|
||||
);
|
||||
}
|
||||
|
||||
const WORKER_OUTPUT_INSTRUCTION = `
|
||||
|
||||
@@ -74,7 +104,9 @@ askUser 每项可为:
|
||||
- outputs 的 key 必须是要求的 outputTags
|
||||
- **设计.worker集 / 设计.worker集.草稿**:value 必须是 JSON 对象文本(以 { 开头),禁止中文说明、元叙述、提问长文;禁止 YAML
|
||||
- **context-fragment.v1**:必须放在 outputs["<本步 artifact tag>"],禁止把片段当根对象。题目只写在产物「追问」(建议选项 + 示例);顶层 askUser 必须为 **null**。程序会把「追问」挂到询问卡。禁止同一问再抄一份 askUser。禁止写入「创作.当前步骤」等进度指针
|
||||
- **其它产物**:优先同时给 outputs + askUser;有产物时追问挂在产物下(用户可直接接受而不作答)。仅当完全无法产出时才留空 outputs、只填 askUser
|
||||
- **其它产物**:优先同时给 outputs + askUser;有产物时追问挂在产物下(用户可直接接受而不作答)
|
||||
- **残稿也要交**:JSON 不完美、字段不全、截断,只要有可展示正文就必须写入 outputs,交给用户验收(可打回重生成,也可接着改)。不要因为渲染不出就把稿扔掉改成提问
|
||||
- 仅当完全没有可展示正文、只有提问时,才留空 outputs、只填 askUser
|
||||
- 能推断选项时 **必须**给 options(完整句、可改写);不要只丢裸问题逼用户写长段
|
||||
- 若 inputs 中 \`用户.需求\` / \`用户.博弈需求\`(或 book.brief)已有实质内容,禁止 askUser 要求用户重复提供其中已写明的情境、角色、规则等;仅对 genuinely 缺失且无法推断的要点提问
|
||||
- summary 用于界面展示`;
|
||||
@@ -193,31 +225,46 @@ function recoverFragmentOutputs(
|
||||
}
|
||||
}
|
||||
|
||||
function dropUnusableFragmentOutputs(outputs: Record<string, string>): void {
|
||||
/** 进度指针不得当产物;半残 JSON / 缺字段仍保留,交给验收卡。 */
|
||||
function dropProgressPointerOutputs(outputs: Record<string, string>): void {
|
||||
for (const tag of Object.keys(outputs)) {
|
||||
if (isProgressPointerTag(tag)) {
|
||||
delete outputs[tag];
|
||||
continue;
|
||||
}
|
||||
const content = outputs[tag]!;
|
||||
const requireFragment = expectsContextFragmentTag(tag);
|
||||
const looksFragment =
|
||||
isUsableContextFragment(content) ||
|
||||
looksLikeFragmentDoc(tryParseObject(content)) ||
|
||||
/"schema"\s*:\s*"context-fragment\.v1"/.test(content) ||
|
||||
/"技能"\s*:/.test(content);
|
||||
if ((requireFragment || looksFragment) && !isUsableContextFragment(content)) {
|
||||
if (isProgressPointerTag(tag) || !outputs[tag]?.trim()) {
|
||||
delete outputs[tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseObject(text: string): unknown {
|
||||
const extracted = extractJsonObjectText(text) ?? text.trim();
|
||||
try {
|
||||
return JSON.parse(extracted);
|
||||
} catch {
|
||||
return undefined;
|
||||
/** 像结构化残稿(含截断 JSON),应进验收而不是改成提问。 */
|
||||
function looksLikeStructuredDraft(text: string): boolean {
|
||||
const t = text.trim();
|
||||
if (!t) return false;
|
||||
if (t.startsWith("{") || t.startsWith("[")) return true;
|
||||
if (/```(?:json)?/i.test(t) && t.includes("{")) return true;
|
||||
if (/"schema"\s*:/.test(t)) return true;
|
||||
if (/"技能"\s*:/.test(t) || /"正文"\s*:/.test(t)) return true;
|
||||
if (/"inserts"\s*:/.test(t) || /"agents"\s*:/.test(t) || /"play_slots"\s*:/.test(t)) {
|
||||
return true;
|
||||
}
|
||||
if (/"workers"\s*:/.test(t) && t.includes("{")) return true;
|
||||
return Boolean(extractJsonObjectText(t));
|
||||
}
|
||||
|
||||
/** 几乎只有提问、没有稿。 */
|
||||
function isQuestionOnlyText(text: string): boolean {
|
||||
const t = text.trim();
|
||||
if (!t) return true;
|
||||
if (looksLikeStructuredDraft(t)) return false;
|
||||
const qs = extractQuestionsFromText(t);
|
||||
if (!qs.length) return false;
|
||||
return looksLikeProseNotSpec(t) || /你的选择|请(?:描述|选择|确认|补充)/.test(t);
|
||||
}
|
||||
|
||||
function canonicalizeKnownOutputs(outputs: Record<string, string>): void {
|
||||
const raw = outputs[CONTEXT_ORDER_TAG];
|
||||
if (!raw?.trim()) return;
|
||||
const doc = parseContextOrder(raw);
|
||||
if (doc?.slots.length) {
|
||||
outputs[CONTEXT_ORDER_TAG] = serializeContextOrder(doc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,20 +286,14 @@ function parseWorkerResponse(
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
if (outputTags.some((t) => WORKER_SET_OUTPUT_TAGS.has(t))) {
|
||||
const questions = normalizeQuestions(extractQuestionsFromText(raw));
|
||||
return {
|
||||
outputs: {},
|
||||
summary: "未产出合法协议 JSON",
|
||||
preview: raw.slice(0, 600),
|
||||
askUser: questions.length
|
||||
? questions.slice(0, 2)
|
||||
: normalizeQuestions([
|
||||
"请补充设计所需的关键信息(上一次未产出合法 JSON 规格)。",
|
||||
]),
|
||||
};
|
||||
}
|
||||
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||
if (target && looksLikeStructuredDraft(raw)) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [target]: raw },
|
||||
{ summary: "格式不完整,已交出残稿" },
|
||||
outputTags,
|
||||
);
|
||||
}
|
||||
if (target && isUsableContextFragment(raw)) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [target]: raw },
|
||||
@@ -260,6 +301,13 @@ function parseWorkerResponse(
|
||||
outputTags,
|
||||
);
|
||||
}
|
||||
if (target && raw.trim() && !isQuestionOnlyText(raw)) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [target]: raw },
|
||||
{ summary: "未按协议包一层,已交出原文" },
|
||||
outputTags,
|
||||
);
|
||||
}
|
||||
const questions = normalizeQuestions(extractQuestionsFromText(raw));
|
||||
return {
|
||||
outputs: {},
|
||||
@@ -323,7 +371,7 @@ function finalizeParsedOutputs(
|
||||
}
|
||||
}
|
||||
|
||||
// 先抽追问,再丢掉半残片段——无正文时仍要把题留给 LLM loop
|
||||
// 先抽追问;半残稿仍留在 outputs,追问挂验收卡(不因缺字段丢掉正文)
|
||||
let askAssessment: string | undefined;
|
||||
const fragQuestions: QuestionItem[] = [];
|
||||
for (const content of Object.values(outputs)) {
|
||||
@@ -339,7 +387,8 @@ function finalizeParsedOutputs(
|
||||
askUser = mergeQuestionsPreferFragment(askUser, fragQuestions);
|
||||
}
|
||||
|
||||
dropUnusableFragmentOutputs(outputs);
|
||||
dropProgressPointerOutputs(outputs);
|
||||
canonicalizeKnownOutputs(outputs);
|
||||
|
||||
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
||||
askUser = normalizeQuestions(INCOMPLETE_FRAGMENT_ASK);
|
||||
@@ -351,11 +400,7 @@ function finalizeParsedOutputs(
|
||||
: Object.values(outputs)[0]?.slice(0, 80) ??
|
||||
(askUser?.length ? `待补充:${askUser[0]!.prompt.slice(0, 40)}` : "Worker 已完成");
|
||||
|
||||
const preview =
|
||||
Object.entries(outputs)
|
||||
.map(([tag, v]) => `### ${tag}\n\n${v}`)
|
||||
.join("\n\n")
|
||||
.slice(0, 4000) || summary;
|
||||
const preview = formatWorkerPreview(outputs, summary);
|
||||
|
||||
return sanitizeWorkerSetOutputs({
|
||||
outputs,
|
||||
@@ -366,12 +411,11 @@ function finalizeParsedOutputs(
|
||||
});
|
||||
}
|
||||
|
||||
/** 规格 tag 必须是可用 JSON;非法散文改为 askUser,避免污染黑板 */
|
||||
/** 规格 tag:能解析则收成 JSON;半残 JSON 保留进验收;纯提问才改成 askUser。 */
|
||||
export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResult {
|
||||
const outputs = { ...result.outputs };
|
||||
const askUser = [...(result.askUser ?? [])];
|
||||
const askAssessment = result.askAssessment;
|
||||
let droppedProse = false;
|
||||
|
||||
for (const tag of [...Object.keys(outputs)]) {
|
||||
if (!WORKER_SET_OUTPUT_TAGS.has(tag)) continue;
|
||||
@@ -388,27 +432,16 @@ export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResu
|
||||
: (extractJsonObjectText(extracted) ?? extracted);
|
||||
continue;
|
||||
}
|
||||
droppedProse = true;
|
||||
delete outputs[tag];
|
||||
if (askUser.length === 0) {
|
||||
const qs = normalizeQuestions(extractQuestionsFromText(content));
|
||||
if (qs.length) askUser.push(...qs.slice(0, 2));
|
||||
if (looksLikeStructuredDraft(content)) {
|
||||
outputs[tag] = content.trim();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (droppedProse && askUser.length === 0) {
|
||||
askUser.push(
|
||||
...normalizeQuestions([
|
||||
"请补充或确认开局关键前提(上一次把说明文字写进了规格字段,未产出合法 JSON)。",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// 提问时不要夹带半残规格
|
||||
if (askUser.length > 0) {
|
||||
for (const tag of WORKER_SET_OUTPUT_TAGS) {
|
||||
// 保留仍合法的草稿;已在上面删掉非法的
|
||||
void tag;
|
||||
if (isQuestionOnlyText(content)) {
|
||||
delete outputs[tag];
|
||||
if (askUser.length === 0) {
|
||||
const qs = normalizeQuestions(extractQuestionsFromText(content));
|
||||
if (qs.length) askUser.push(...qs.slice(0, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,10 +453,7 @@ export function sanitizeWorkerSetOutputs(result: WorkerRunResult): WorkerRunResu
|
||||
const preview =
|
||||
askUser.length && Object.keys(outputs).length === 0
|
||||
? askUser.map((q) => `- ${q.prompt}`).join("\n")
|
||||
: Object.entries(outputs)
|
||||
.map(([tag, v]) => `### ${tag}\n\n${v}`)
|
||||
.join("\n\n")
|
||||
.slice(0, 4000) || summary;
|
||||
: formatWorkerPreview(outputs, summary);
|
||||
|
||||
return {
|
||||
outputs,
|
||||
@@ -527,22 +557,41 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
inputs[CONTEXT_BRIEF_TAG] = priorBrief;
|
||||
}
|
||||
|
||||
const userPayload = assembleWorkerContext({
|
||||
inputs,
|
||||
segments: worker.contextSegments,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
workerId: worker.id,
|
||||
workerName: worker.name,
|
||||
outputTags: worker.outputTags,
|
||||
});
|
||||
const systemContent = promptBody + WORKER_OUTPUT_INSTRUCTION;
|
||||
const playPreset = isPlayLayerActive(params.slots)
|
||||
? resolveActivePreset(loadAppSettings().activePresetId)
|
||||
: null;
|
||||
|
||||
const messages = playPreset
|
||||
? assemblePlayWorkerMessages({
|
||||
systemPrompt: systemContent,
|
||||
preset: playPreset,
|
||||
pack: worldInfoPackFromSegments({
|
||||
segments: worker.contextSegments ?? [],
|
||||
inputs,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
}),
|
||||
})
|
||||
: [
|
||||
{ role: "system" as const, content: systemContent },
|
||||
{
|
||||
role: "user" as const,
|
||||
content: assembleWorkerContext({
|
||||
inputs,
|
||||
segments: worker.contextSegments,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
workerId: worker.id,
|
||||
workerName: worker.name,
|
||||
outputTags: worker.outputTags,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const result = await completeWorkerPreferStream(
|
||||
params.llm,
|
||||
[
|
||||
{ role: "system", content: promptBody + WORKER_OUTPUT_INSTRUCTION },
|
||||
{ role: "user", content: userPayload },
|
||||
],
|
||||
messages,
|
||||
{
|
||||
responseFormat: "json_object",
|
||||
caller: `worker:${worker.id}`,
|
||||
|
||||
Reference in New Issue
Block a user