新增转述回合引擎,用插入工具把对话落成带相对序的产物。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import type { LlmProvider } from "../llm/client.js";
|
||||
import {
|
||||
createLocalLlmDriver,
|
||||
type AgentDriver,
|
||||
} from "../runtime/driver.js";
|
||||
import { buildDictateMessages } from "./context.js";
|
||||
import { buildDictateSystemPrompt } from "./prompt.js";
|
||||
import { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
|
||||
import {
|
||||
isAllowedProductTag,
|
||||
parseDictateOrder,
|
||||
type DictateChatTurn,
|
||||
type DictateProduct,
|
||||
} from "./types.js";
|
||||
|
||||
export type DictateTurnHandlers = {
|
||||
listProducts: () => DictateProduct[];
|
||||
/** order 省略时由实现沿用原序或默认 */
|
||||
writeProduct: (tag: string, content: string, order?: number) => void;
|
||||
clearDialogue: () => void;
|
||||
onToolCall?: (name: string, detail: string) => void;
|
||||
onThinkingDelta?: (delta: string) => void;
|
||||
onThinkingDone?: (text: string) => void;
|
||||
};
|
||||
|
||||
export type DictateTurnResult = {
|
||||
reply: string;
|
||||
iterations: number;
|
||||
wroteTags: string[];
|
||||
clearedDialogue: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 跑一轮转述整理:上下文 = 预设提示 + 按相对序的产物 + 全量对话。
|
||||
* 产物只许 insert(position, content, order?);最终文本即对用户回复。
|
||||
*/
|
||||
export async function runDictateTurn(params: {
|
||||
llm: LlmProvider;
|
||||
dialogue: DictateChatTurn[];
|
||||
handlers: DictateTurnHandlers;
|
||||
systemPrompt?: string;
|
||||
recipeName?: string;
|
||||
recipeBrief?: string;
|
||||
driver?: AgentDriver;
|
||||
}): Promise<DictateTurnResult> {
|
||||
const driver = params.driver ?? createLocalLlmDriver(params.llm);
|
||||
const wroteTags: string[] = [];
|
||||
let clearedDialogue = false;
|
||||
let dialogue = [...params.dialogue];
|
||||
|
||||
const systemPrompt =
|
||||
params.systemPrompt ??
|
||||
buildDictateSystemPrompt({
|
||||
recipeName: params.recipeName,
|
||||
recipeBrief: params.recipeBrief,
|
||||
});
|
||||
const assembled = buildDictateMessages({
|
||||
systemPrompt,
|
||||
products: params.handlers.listProducts(),
|
||||
dialogue,
|
||||
});
|
||||
// driver 会再前置 system;此处只传 user 拼装块,避免双重 system
|
||||
const userBlock = assembled.filter((m) => m.role !== "system");
|
||||
|
||||
const run = await driver.run({
|
||||
system: systemPrompt,
|
||||
messages: userBlock,
|
||||
tools: DICTATE_TOOL_DEFINITIONS,
|
||||
caller: "dictate_agent",
|
||||
label: "转述整理",
|
||||
onThinkingDelta: params.handlers.onThinkingDelta,
|
||||
onThinkingDone: params.handlers.onThinkingDone,
|
||||
handleStep: (calls) => {
|
||||
const results = calls.map((call) => {
|
||||
const args = safeParseArgs(call.arguments);
|
||||
if (call.name === "insert" || call.name === "write_product") {
|
||||
const tag = String(
|
||||
args.position ?? args.tag ?? "",
|
||||
).trim();
|
||||
const content = String(args.content ?? "");
|
||||
if (!isAllowedProductTag(tag)) {
|
||||
const err = `拒绝写入:position 必须以「用户.」或「设计.」开头(收到:${tag || "空"})`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
if (!content.trim()) {
|
||||
const err = "content 不能为空";
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
}
|
||||
const order = parseDictateOrder(args.order);
|
||||
params.handlers.writeProduct(tag, content, order);
|
||||
wroteTags.push(tag);
|
||||
const detail =
|
||||
order === undefined ? tag : `${tag} order=${order}`;
|
||||
params.handlers.onToolCall?.(call.name, detail);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
position: tag,
|
||||
...(order !== undefined ? { order } : {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (call.name === "clear_dialogue") {
|
||||
const reason = String(args.reason ?? "").trim() || "上下文过长";
|
||||
params.handlers.clearDialogue();
|
||||
clearedDialogue = true;
|
||||
dialogue = [];
|
||||
params.handlers.onToolCall?.(call.name, reason);
|
||||
return {
|
||||
callId: call.id,
|
||||
content: JSON.stringify({
|
||||
ok: true,
|
||||
cleared: true,
|
||||
note: "对话已清空;产物仍在。后续上下文以产物为准。",
|
||||
}),
|
||||
};
|
||||
}
|
||||
const err = `未知工具:${call.name}`;
|
||||
params.handlers.onToolCall?.(call.name, err);
|
||||
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||
});
|
||||
return { kind: "continue" as const, results };
|
||||
},
|
||||
});
|
||||
|
||||
// driver 的 messages 在首轮已固定;clear 后若还要续写依赖模型已知工具结果。
|
||||
// 若模型只调工具不说话,给一个兜底回复。
|
||||
let reply = "";
|
||||
if (run.stop.kind === "text") {
|
||||
reply = run.stop.content.trim();
|
||||
}
|
||||
if (!reply) {
|
||||
if (wroteTags.length) {
|
||||
reply = `已写入:${[...new Set(wroteTags)].join("、")}。还有要补充的吗?`;
|
||||
} else if (clearedDialogue) {
|
||||
reply = "对话已清空,产物都还在。可以继续说。";
|
||||
} else {
|
||||
reply = "我在听,请继续说你想要的体验或设定。";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reply,
|
||||
iterations: run.iterations,
|
||||
wroteTags: [...new Set(wroteTags)],
|
||||
clearedDialogue,
|
||||
};
|
||||
}
|
||||
|
||||
function safeParseArgs(raw: string): Record<string, unknown> {
|
||||
try {
|
||||
const parsed = JSON.parse(raw || "{}");
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return {};
|
||||
}
|
||||
Reference in New Issue
Block a user