refactor: extract execution prompt assembly
This commit is contained in:
@@ -296,6 +296,13 @@ export function resolveRelevantMemories(input: {
|
||||
export const resolveRelevantMemoriesForTesting = resolveRelevantMemories;
|
||||
```
|
||||
|
||||
为避免 `master-agent` 当前生产行为回归,允许在同一个 `/Users/kris/code/boss/src/lib/execution/memory-resolver.ts` 中额外提供一个 **runtime-safe helper**(例如 `resolveRuntimeRelevantMemories(...)`),专门给 `boss-master-agent.ts` 使用。约束如下:
|
||||
- `resolveRelevantMemories(...)` 仍保持上面的最小 contract,供基础测试与后续 contract 使用。
|
||||
- runtime-safe helper 只负责把当前已存在的主 Agent 运行时保护内聚回执行模块,例如:
|
||||
- `workflow_rule / user_preference` 优先;
|
||||
- `master-agent` 非空请求但无 lexical 命中时,回退到前 6 个项目记忆。
|
||||
- 不允许把这些运行时保护继续散落在 `boss-master-agent.ts` 中;如果需要保留生产行为,优先放进同一个 execution 模块。
|
||||
|
||||
```ts
|
||||
// /Users/kris/code/boss/src/lib/execution/prompt-assembler.ts
|
||||
import { resolveRelevantMemories } from "@/lib/execution/memory-resolver";
|
||||
|
||||
@@ -21,6 +21,9 @@ import {
|
||||
} from "@/lib/boss-data";
|
||||
import type { AiProvider, DispatchPlanTarget, Project, ProjectAgentControls, ReasoningEffort } from "@/lib/boss-data";
|
||||
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
|
||||
import { resolveRuntimeRelevantMemories } from "@/lib/execution/memory-resolver";
|
||||
import type { RelevantMemory } from "@/lib/execution/memory-resolver";
|
||||
import { buildExecutionPrompt } from "@/lib/execution/prompt-assembler";
|
||||
import { readAliyunOssObjectBuffer } from "@/lib/boss-storage-aliyun-oss";
|
||||
import { readServerFileAttachmentBuffer } from "@/lib/boss-storage-server-file";
|
||||
import {
|
||||
@@ -90,8 +93,11 @@ export async function resolveMasterAgentExecutionConfig(
|
||||
const promptPolicy = getMasterAgentPromptPolicyView(state);
|
||||
const userPrompt = getUserMasterPromptView(state, resolvedAccountId);
|
||||
const memoryScope = listUserMasterMemoriesView(state, resolvedAccountId, { includeArchived: false });
|
||||
const projectMemories = selectRelevantProjectMemories(memoryScope, projectId, requestText);
|
||||
const userMemories = selectRelevantUserMemories(memoryScope, requestText);
|
||||
const { projectMemories, userMemories } = resolveRuntimeRelevantMemories({
|
||||
projectId,
|
||||
requestText,
|
||||
memories: memoryScope,
|
||||
});
|
||||
const touchedMemoryIds = [...projectMemories, ...userMemories].map((memory) => memory.memoryId);
|
||||
if (touchedMemoryIds.length > 0) {
|
||||
await touchUserMasterMemories(touchedMemoryIds, resolvedAccountId);
|
||||
@@ -109,84 +115,17 @@ export async function resolveMasterAgentExecutionConfig(
|
||||
userPrompt,
|
||||
projectMemories,
|
||||
userMemories,
|
||||
executionPrompt: buildMasterAgentExecutionPrompt({
|
||||
state,
|
||||
projectId,
|
||||
requestText: requestText ?? "",
|
||||
currentSessionExpiresAt: undefined,
|
||||
agentControls: scopedAgentControls,
|
||||
accountId: resolvedAccountId,
|
||||
promptPolicy,
|
||||
userPrompt,
|
||||
executionPrompt: buildExecutionPrompt({
|
||||
globalPrompt: promptPolicy?.globalPrompt ?? null,
|
||||
userPrompt: userPrompt?.content ?? null,
|
||||
conversationPrompt: scopedAgentControls?.promptOverride ?? null,
|
||||
projectMemories,
|
||||
userMemories,
|
||||
requestText: requestText ?? "",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function selectRelevantProjectMemories(
|
||||
memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
|
||||
projectId: string,
|
||||
requestText?: string,
|
||||
) {
|
||||
const projectScoped = memories.filter((memory) => memory.scope === "project");
|
||||
if (projectId !== "master-agent") {
|
||||
return projectScoped.filter((memory) => memory.projectId === projectId);
|
||||
}
|
||||
if (projectScoped.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowered = requestText?.trim().toLowerCase() ?? "";
|
||||
if (!lowered) {
|
||||
return projectScoped.slice(0, 6);
|
||||
}
|
||||
|
||||
const matched = projectScoped.filter((memory) => {
|
||||
const haystacks = [memory.title, memory.content, ...(memory.tags ?? [])]
|
||||
.map((value) => value.toLowerCase());
|
||||
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
|
||||
});
|
||||
|
||||
return (matched.length > 0 ? matched : projectScoped).slice(0, 6);
|
||||
}
|
||||
|
||||
function selectRelevantUserMemories(
|
||||
memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
|
||||
requestText?: string,
|
||||
) {
|
||||
const globalScoped = memories.filter((memory) => memory.scope === "global");
|
||||
if (globalScoped.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowered = requestText?.trim().toLowerCase() ?? "";
|
||||
const prioritized = [...globalScoped].sort((left, right) => {
|
||||
const leftPriority =
|
||||
left.memoryType === "workflow_rule" || left.memoryType === "user_preference" ? 1 : 0;
|
||||
const rightPriority =
|
||||
right.memoryType === "workflow_rule" || right.memoryType === "user_preference" ? 1 : 0;
|
||||
if (leftPriority !== rightPriority) {
|
||||
return rightPriority - leftPriority;
|
||||
}
|
||||
const leftTime = Date.parse(left.lastUsedAt ?? left.updatedAt ?? left.createdAt) || 0;
|
||||
const rightTime = Date.parse(right.lastUsedAt ?? right.updatedAt ?? right.createdAt) || 0;
|
||||
return rightTime - leftTime;
|
||||
});
|
||||
|
||||
if (!lowered) {
|
||||
return prioritized.slice(0, 8);
|
||||
}
|
||||
|
||||
const matched = prioritized.filter((memory) => {
|
||||
const haystacks = [memory.title, memory.content, ...(memory.tags ?? [])]
|
||||
.map((value) => value.toLowerCase());
|
||||
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
|
||||
});
|
||||
|
||||
return (matched.length > 0 ? matched : prioritized).slice(0, 8);
|
||||
}
|
||||
|
||||
function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
|
||||
if (!agentControls) {
|
||||
return "当前对话覆盖:无";
|
||||
@@ -200,63 +139,28 @@ function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function buildPromptPolicyDigest(promptPolicy: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>) {
|
||||
return promptPolicy?.globalPrompt?.trim()
|
||||
? [`管理员全局主提示词:`, promptPolicy.globalPrompt.trim()].join("\n")
|
||||
: "管理员全局主提示词:无";
|
||||
}
|
||||
|
||||
function buildUserPromptDigest(userPrompt: Awaited<ReturnType<typeof getUserMasterPromptView>>) {
|
||||
return userPrompt?.content?.trim()
|
||||
? [`用户私有主提示词:`, userPrompt.content.trim()].join("\n")
|
||||
: "用户私有主提示词:无";
|
||||
}
|
||||
|
||||
function buildMemoryDigest(title: string, memories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>) {
|
||||
if (memories.length === 0) {
|
||||
return `${title}:无`;
|
||||
}
|
||||
return [
|
||||
`${title}:`,
|
||||
...memories.map((memory) => {
|
||||
const meta = [
|
||||
memory.scope === "project" && memory.projectId ? `projectId=${memory.projectId}` : null,
|
||||
memory.memoryType ? `type=${memory.memoryType}` : null,
|
||||
memory.tags?.length ? `tags=${memory.tags.join("|")}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
return meta
|
||||
? `- ${memory.title}(${meta}):${memory.content}`
|
||||
: `- ${memory.title}:${memory.content}`;
|
||||
}),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildMasterAgentExecutionPrompt(params: {
|
||||
state: Awaited<ReturnType<typeof readState>>;
|
||||
projectId: string;
|
||||
requestText: string;
|
||||
currentSessionExpiresAt?: string;
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
accountId: string;
|
||||
promptPolicy: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
|
||||
userPrompt: Awaited<ReturnType<typeof getUserMasterPromptView>>;
|
||||
projectMemories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
userMemories: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
projectMemories: RelevantMemory[];
|
||||
userMemories: RelevantMemory[];
|
||||
}) {
|
||||
return [
|
||||
buildMasterAgentInstructions(),
|
||||
buildPromptPolicyDigest(params.promptPolicy),
|
||||
buildUserPromptDigest(params.userPrompt),
|
||||
params.agentControls?.promptOverride?.trim()
|
||||
? ["当前对话附加提示词:", params.agentControls.promptOverride.trim()].join("\n")
|
||||
: "当前对话附加提示词:无",
|
||||
buildMemoryDigest("项目记忆", params.projectMemories),
|
||||
buildMemoryDigest("用户记忆", params.userMemories),
|
||||
buildExecutionPrompt({
|
||||
globalPrompt: params.promptPolicy?.globalPrompt ?? "",
|
||||
userPrompt: params.userPrompt?.content ?? "",
|
||||
conversationPrompt: params.agentControls?.promptOverride ?? "",
|
||||
projectMemories: params.projectMemories,
|
||||
userMemories: params.userMemories,
|
||||
requestText: params.requestText,
|
||||
}),
|
||||
buildAgentControlsDigest(params.agentControls),
|
||||
"",
|
||||
buildRuntimeDigest(params.state, params.requestText, params.currentSessionExpiresAt, params.agentControls),
|
||||
buildRuntimeDigest(params.state, params.requestText, params.currentSessionExpiresAt),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
@@ -292,7 +196,6 @@ function buildRuntimeDigest(
|
||||
state: Awaited<ReturnType<typeof readState>>,
|
||||
requestText: string,
|
||||
currentSessionExpiresAt?: string,
|
||||
agentControls?: ProjectAgentControls | null,
|
||||
) {
|
||||
const recentMessages = state.projects
|
||||
.find((project) => project.id === "master-agent")
|
||||
@@ -331,7 +234,6 @@ function buildRuntimeDigest(
|
||||
`登录会话策略:成功登录后默认保持 ${Math.round(AUTH_SESSION_TTL_MS / 24 / 60 / 60_000)} 天。`,
|
||||
"Cookie Max-Age:2592000 秒。",
|
||||
currentSessionExpiresAt ? `当前请求会话到期时间:${currentSessionExpiresAt}` : undefined,
|
||||
buildAgentControlsDigest(agentControls),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
@@ -499,8 +401,8 @@ async function replyViaOpenAiAccount(params: {
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
|
||||
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
|
||||
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
projectMemories?: RelevantMemory[];
|
||||
userMemories?: RelevantMemory[];
|
||||
}) {
|
||||
if (!params.account?.apiKey?.trim() || !isApiCompatibleProvider(params.account.provider)) {
|
||||
throw new Error("OPENAI_ACCOUNT_NOT_CONFIGURED");
|
||||
@@ -548,31 +450,29 @@ async function generateApiProviderReply(params: {
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
|
||||
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
|
||||
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
projectMemories?: RelevantMemory[];
|
||||
userMemories?: RelevantMemory[];
|
||||
}) {
|
||||
const state = await readState();
|
||||
const effectiveProjectMemories =
|
||||
params.projectMemories && params.projectMemories.length > 0
|
||||
? params.projectMemories
|
||||
: selectRelevantProjectMemories(
|
||||
listUserMasterMemoriesView(state, params.userPrompt?.account ?? state.user.account, {
|
||||
: resolveRuntimeRelevantMemories({
|
||||
projectId: "master-agent",
|
||||
requestText: params.requestText,
|
||||
memories: listUserMasterMemoriesView(state, params.userPrompt?.account ?? state.user.account, {
|
||||
includeArchived: false,
|
||||
}),
|
||||
"master-agent",
|
||||
params.requestText,
|
||||
);
|
||||
}).projectMemories;
|
||||
let response: Response;
|
||||
const config = apiProviderConfig(params.provider);
|
||||
const requestBody: Record<string, unknown> = {
|
||||
model: params.model,
|
||||
instructions: buildMasterAgentExecutionPrompt({
|
||||
state,
|
||||
projectId: "master-agent",
|
||||
requestText: params.requestText,
|
||||
currentSessionExpiresAt: params.currentSessionExpiresAt,
|
||||
agentControls: params.agentControls,
|
||||
accountId: "master-agent",
|
||||
promptPolicy: params.promptPolicy ?? null,
|
||||
userPrompt: params.userPrompt ?? null,
|
||||
projectMemories: effectiveProjectMemories,
|
||||
@@ -638,16 +538,14 @@ function buildMasterOpenAiReplyPrompt(
|
||||
agentControls?: ProjectAgentControls | null,
|
||||
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>,
|
||||
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>,
|
||||
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
|
||||
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
|
||||
projectMemories?: RelevantMemory[],
|
||||
userMemories?: RelevantMemory[],
|
||||
) {
|
||||
return buildMasterAgentExecutionPrompt({
|
||||
state,
|
||||
projectId: "master-agent",
|
||||
requestText,
|
||||
currentSessionExpiresAt,
|
||||
agentControls,
|
||||
accountId: "master-agent",
|
||||
promptPolicy: promptPolicy ?? null,
|
||||
userPrompt: userPrompt ?? null,
|
||||
projectMemories: projectMemories ?? [],
|
||||
@@ -667,8 +565,8 @@ async function queueAndStartOpenAiMasterAgentReply(params: {
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
|
||||
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
|
||||
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
projectMemories?: RelevantMemory[];
|
||||
userMemories?: RelevantMemory[];
|
||||
}) {
|
||||
const timer = setTimeout(() => {
|
||||
void (async () => {
|
||||
@@ -727,8 +625,8 @@ async function enqueueOpenAiMasterAgentReply(params: {
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>;
|
||||
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>;
|
||||
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
|
||||
projectMemories?: RelevantMemory[];
|
||||
userMemories?: RelevantMemory[];
|
||||
}) {
|
||||
const state = await readState();
|
||||
const task = await queueMasterAgentTask({
|
||||
@@ -866,16 +764,14 @@ function buildMasterCodexNodePrompt(
|
||||
agentControls?: ProjectAgentControls | null,
|
||||
promptPolicy?: Awaited<ReturnType<typeof getMasterAgentPromptPolicyView>>,
|
||||
userPrompt?: Awaited<ReturnType<typeof getUserMasterPromptView>>,
|
||||
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
|
||||
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>,
|
||||
projectMemories?: RelevantMemory[],
|
||||
userMemories?: RelevantMemory[],
|
||||
) {
|
||||
return buildMasterAgentExecutionPrompt({
|
||||
state,
|
||||
projectId: "master-agent",
|
||||
requestText,
|
||||
currentSessionExpiresAt,
|
||||
agentControls,
|
||||
accountId: "master-agent",
|
||||
promptPolicy: promptPolicy ?? null,
|
||||
userPrompt: userPrompt ?? null,
|
||||
projectMemories: projectMemories ?? [],
|
||||
|
||||
88
src/lib/execution/memory-resolver.ts
Normal file
88
src/lib/execution/memory-resolver.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { MasterAgentMemory } from "@/lib/boss-data";
|
||||
|
||||
export type RelevantMemory = Pick<
|
||||
MasterAgentMemory,
|
||||
"memoryId" | "scope" | "projectId" | "title" | "content" | "tags" | "memoryType" | "lastUsedAt" | "updatedAt" | "createdAt"
|
||||
>;
|
||||
|
||||
export function resolveRelevantMemories(input: {
|
||||
projectId: string;
|
||||
requestText?: string;
|
||||
memories: RelevantMemory[];
|
||||
}) {
|
||||
const lowered = input.requestText?.trim().toLowerCase() ?? "";
|
||||
const projectScoped = input.memories.filter((memory) => {
|
||||
if (memory.scope !== "project") {
|
||||
return false;
|
||||
}
|
||||
if (input.projectId !== "master-agent") {
|
||||
return memory.projectId === input.projectId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const projectMemories =
|
||||
input.projectId !== "master-agent"
|
||||
? projectScoped.slice(0, 6)
|
||||
: !lowered
|
||||
? projectScoped.slice(0, 6)
|
||||
: projectScoped
|
||||
.filter((memory) => {
|
||||
const haystacks = [memory.projectId, memory.title, memory.content, ...(memory.tags ?? [])]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((value) => value.toLowerCase());
|
||||
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
|
||||
})
|
||||
.slice(0, 6);
|
||||
|
||||
const userMemories = input.memories.filter((memory) => memory.scope === "global").slice(0, 8);
|
||||
|
||||
return {
|
||||
projectMemories,
|
||||
userMemories,
|
||||
};
|
||||
}
|
||||
|
||||
function prioritizeRuntimeMemories(memories: RelevantMemory[]) {
|
||||
const globalMemories = memories
|
||||
.filter((memory) => memory.scope === "global")
|
||||
.sort((left, right) => {
|
||||
const leftPriority =
|
||||
left.memoryType === "workflow_rule" || left.memoryType === "user_preference" ? 1 : 0;
|
||||
const rightPriority =
|
||||
right.memoryType === "workflow_rule" || right.memoryType === "user_preference" ? 1 : 0;
|
||||
if (leftPriority !== rightPriority) {
|
||||
return rightPriority - leftPriority;
|
||||
}
|
||||
|
||||
const leftTime = Date.parse(left.lastUsedAt ?? left.updatedAt ?? left.createdAt) || 0;
|
||||
const rightTime = Date.parse(right.lastUsedAt ?? right.updatedAt ?? right.createdAt) || 0;
|
||||
return rightTime - leftTime;
|
||||
});
|
||||
const projectMemories = memories.filter((memory) => memory.scope === "project");
|
||||
return [...globalMemories, ...projectMemories];
|
||||
}
|
||||
|
||||
export function resolveRuntimeRelevantMemories(input: {
|
||||
projectId: string;
|
||||
requestText?: string;
|
||||
memories: RelevantMemory[];
|
||||
}) {
|
||||
const prioritizedMemories = prioritizeRuntimeMemories(input.memories);
|
||||
const resolved = resolveRelevantMemories({
|
||||
projectId: input.projectId,
|
||||
requestText: input.requestText,
|
||||
memories: prioritizedMemories,
|
||||
});
|
||||
|
||||
if (input.projectId === "master-agent" && input.requestText?.trim() && resolved.projectMemories.length === 0) {
|
||||
return {
|
||||
projectMemories: prioritizedMemories.filter((memory) => memory.scope === "project").slice(0, 6),
|
||||
userMemories: resolved.userMemories,
|
||||
};
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export const resolveRelevantMemoriesForTesting = resolveRelevantMemories;
|
||||
export const resolveRuntimeRelevantMemoriesForTesting = resolveRuntimeRelevantMemories;
|
||||
48
src/lib/execution/prompt-assembler.ts
Normal file
48
src/lib/execution/prompt-assembler.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { MasterAgentMemory } from "@/lib/boss-data";
|
||||
|
||||
type PromptMemory = Pick<MasterAgentMemory, "projectId" | "title" | "content" | "tags">;
|
||||
|
||||
function buildProjectMemorySection(memories: PromptMemory[]) {
|
||||
if (memories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
"项目记忆:",
|
||||
...memories.map((memory) => `- [${memory.projectId ?? "unknown"}] ${memory.title}: ${memory.content}`),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildUserMemorySection(memories: PromptMemory[]) {
|
||||
if (memories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
"用户通用记忆:",
|
||||
...memories.map((memory) => `- ${memory.title}: ${memory.content}`),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildExecutionPrompt(input: {
|
||||
globalPrompt?: string | null;
|
||||
userPrompt?: string | null;
|
||||
conversationPrompt?: string | null;
|
||||
projectMemories: PromptMemory[];
|
||||
userMemories: PromptMemory[];
|
||||
requestText: string;
|
||||
}) {
|
||||
return [
|
||||
input.globalPrompt?.trim() ? `管理员全局主提示词:\n${input.globalPrompt.trim()}` : null,
|
||||
input.userPrompt?.trim() ? `用户私有主提示词:\n${input.userPrompt.trim()}` : null,
|
||||
input.conversationPrompt?.trim() ? `当前对话附加提示词:\n${input.conversationPrompt.trim()}` : null,
|
||||
buildProjectMemorySection(input.projectMemories),
|
||||
buildUserMemorySection(input.userMemories),
|
||||
`当前消息:\n${input.requestText}`,
|
||||
]
|
||||
.filter((value): value is string => value !== null)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
export const buildExecutionPromptForTesting = buildExecutionPrompt;
|
||||
export { resolveRelevantMemoriesForTesting } from "@/lib/execution/memory-resolver";
|
||||
138
tests/execution-memory-resolver.test.ts
Normal file
138
tests/execution-memory-resolver.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
resolveRelevantMemoriesForTesting,
|
||||
resolveRuntimeRelevantMemoriesForTesting,
|
||||
} from "@/lib/execution/memory-resolver";
|
||||
|
||||
test("MemoryResolver 在 master-agent 会话下优先挑当前请求命中的项目记忆", () => {
|
||||
const resolved = resolveRelevantMemoriesForTesting({
|
||||
projectId: "master-agent",
|
||||
requestText: "boss-console 的审批流",
|
||||
memories: [
|
||||
{
|
||||
memoryId: "m1",
|
||||
scope: "project",
|
||||
projectId: "boss-console",
|
||||
title: "审批流",
|
||||
content: "boss-console approval",
|
||||
tags: ["approval"],
|
||||
memoryType: "project_progress",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
memoryId: "m2",
|
||||
scope: "project",
|
||||
projectId: "wenshenapp",
|
||||
title: "UI",
|
||||
content: "wechat ui",
|
||||
tags: ["ui"],
|
||||
memoryType: "project_progress",
|
||||
createdAt: "2026-01-02T00:00:00.000Z",
|
||||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(resolved.projectMemories.length, 1);
|
||||
assert.equal(resolved.projectMemories[0]?.projectId, "boss-console");
|
||||
});
|
||||
|
||||
test("MemoryResolver 会保留全局记忆的输入顺序并只截断到 8 条", () => {
|
||||
const resolved = resolveRelevantMemoriesForTesting({
|
||||
projectId: "master-agent",
|
||||
memories: [
|
||||
{
|
||||
memoryId: "g1",
|
||||
scope: "global",
|
||||
title: "一",
|
||||
content: "one",
|
||||
tags: [],
|
||||
memoryType: "decision",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
memoryId: "g2",
|
||||
scope: "global",
|
||||
title: "二",
|
||||
content: "two",
|
||||
tags: [],
|
||||
memoryType: "decision",
|
||||
createdAt: "2026-01-02T00:00:00.000Z",
|
||||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
resolved.userMemories.map((memory) => memory.memoryId),
|
||||
["g1", "g2"],
|
||||
);
|
||||
});
|
||||
|
||||
test("Runtime MemoryResolver 会优先排布 workflow_rule 和 user_preference 全局记忆", () => {
|
||||
const resolved = resolveRuntimeRelevantMemoriesForTesting({
|
||||
projectId: "master-agent",
|
||||
memories: [
|
||||
{
|
||||
memoryId: "g1",
|
||||
scope: "global",
|
||||
title: "普通记忆",
|
||||
content: "normal",
|
||||
tags: [],
|
||||
memoryType: "decision",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
memoryId: "g2",
|
||||
scope: "global",
|
||||
title: "规则记忆",
|
||||
content: "workflow",
|
||||
tags: [],
|
||||
memoryType: "workflow_rule",
|
||||
createdAt: "2026-01-03T00:00:00.000Z",
|
||||
updatedAt: "2026-01-03T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
memoryId: "g3",
|
||||
scope: "global",
|
||||
title: "偏好记忆",
|
||||
content: "preference",
|
||||
tags: [],
|
||||
memoryType: "user_preference",
|
||||
createdAt: "2026-01-02T00:00:00.000Z",
|
||||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
resolved.userMemories.map((memory) => memory.memoryId),
|
||||
["g2", "g3", "g1"],
|
||||
);
|
||||
});
|
||||
|
||||
test("Runtime MemoryResolver 在 master-agent 非空请求但无 lexical 命中时回退到前 6 个项目记忆", () => {
|
||||
const resolved = resolveRuntimeRelevantMemoriesForTesting({
|
||||
projectId: "master-agent",
|
||||
requestText: "xyzxyz no overlap",
|
||||
memories: [
|
||||
{ memoryId: "p1", scope: "project", projectId: "boss-console", title: "alpha", content: "alpha-content", tags: [], memoryType: "project_progress", createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
|
||||
{ memoryId: "p2", scope: "project", projectId: "boss-console", title: "bravo", content: "bravo-content", tags: [], memoryType: "project_progress", createdAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" },
|
||||
{ memoryId: "p3", scope: "project", projectId: "boss-console", title: "charlie", content: "charlie-content", tags: [], memoryType: "project_progress", createdAt: "2026-01-03T00:00:00.000Z", updatedAt: "2026-01-03T00:00:00.000Z" },
|
||||
{ memoryId: "p4", scope: "project", projectId: "boss-console", title: "delta", content: "delta-content", tags: [], memoryType: "project_progress", createdAt: "2026-01-04T00:00:00.000Z", updatedAt: "2026-01-04T00:00:00.000Z" },
|
||||
{ memoryId: "p5", scope: "project", projectId: "boss-console", title: "echo", content: "echo-content", tags: [], memoryType: "project_progress", createdAt: "2026-01-05T00:00:00.000Z", updatedAt: "2026-01-05T00:00:00.000Z" },
|
||||
{ memoryId: "p6", scope: "project", projectId: "boss-console", title: "foxtrot", content: "foxtrot-content", tags: [], memoryType: "project_progress", createdAt: "2026-01-06T00:00:00.000Z", updatedAt: "2026-01-06T00:00:00.000Z" },
|
||||
{ memoryId: "p7", scope: "project", projectId: "boss-console", title: "golf", content: "golf-content", tags: [], memoryType: "project_progress", createdAt: "2026-01-07T00:00:00.000Z", updatedAt: "2026-01-07T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(resolved.projectMemories.length, 6);
|
||||
assert.deepEqual(
|
||||
resolved.projectMemories.map((memory) => memory.memoryId),
|
||||
["p1", "p2", "p3", "p4", "p5", "p6"],
|
||||
);
|
||||
});
|
||||
63
tests/execution-prompt-assembler.test.ts
Normal file
63
tests/execution-prompt-assembler.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
buildExecutionPromptForTesting,
|
||||
resolveRelevantMemoriesForTesting,
|
||||
} from "@/lib/execution/prompt-assembler";
|
||||
|
||||
test("PromptAssembler 会按固定顺序拼管理员提示词、用户提示词、对话提示词和记忆", () => {
|
||||
const prompt = buildExecutionPromptForTesting({
|
||||
globalPrompt: "GLOBAL",
|
||||
userPrompt: "USER",
|
||||
conversationPrompt: "CONVERSATION",
|
||||
projectMemories: [{ title: "项目记忆", content: "PROJECT", projectId: "boss-console", tags: [] }],
|
||||
userMemories: [{ title: "用户记忆", content: "USER_MEMORY", tags: [] }],
|
||||
requestText: "继续",
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
prompt,
|
||||
[
|
||||
"管理员全局主提示词:\nGLOBAL",
|
||||
"用户私有主提示词:\nUSER",
|
||||
"当前对话附加提示词:\nCONVERSATION",
|
||||
"项目记忆:\n- [boss-console] 项目记忆: PROJECT",
|
||||
"用户通用记忆:\n- 用户记忆: USER_MEMORY",
|
||||
"当前消息:\n继续",
|
||||
].join("\n\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("PromptAssembler 会透出可测试的记忆筛选器", () => {
|
||||
const resolved = resolveRelevantMemoriesForTesting({
|
||||
projectId: "master-agent",
|
||||
requestText: "boss-console 的审批流",
|
||||
memories: [
|
||||
{
|
||||
memoryId: "m1",
|
||||
scope: "project",
|
||||
projectId: "boss-console",
|
||||
title: "审批流",
|
||||
content: "boss-console approval",
|
||||
tags: ["approval"],
|
||||
memoryType: "project_progress",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
memoryId: "m2",
|
||||
scope: "project",
|
||||
projectId: "wenshenapp",
|
||||
title: "UI",
|
||||
content: "wechat ui",
|
||||
tags: ["ui"],
|
||||
memoryType: "project_progress",
|
||||
createdAt: "2026-01-02T00:00:00.000Z",
|
||||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(resolved.projectMemories.length, 1);
|
||||
assert.equal(resolved.projectMemories[0]?.projectId, "boss-console");
|
||||
});
|
||||
@@ -11,6 +11,8 @@ let updateMasterAgentPromptPolicy: (typeof import("../src/lib/boss-data"))["upda
|
||||
let updateUserMasterPrompt: (typeof import("../src/lib/boss-data"))["updateUserMasterPrompt"];
|
||||
let createUserMasterMemory: (typeof import("../src/lib/boss-data"))["createUserMasterMemory"];
|
||||
let resolveMasterAgentExecutionConfig: (typeof import("../src/lib/boss-master-agent"))["resolveMasterAgentExecutionConfig"];
|
||||
let stateFile = "";
|
||||
let stateBackupFile = "";
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
@@ -18,6 +20,8 @@ async function setup() {
|
||||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-config-"));
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
stateFile = process.env.BOSS_STATE_FILE;
|
||||
stateBackupFile = `${stateFile}.bak`;
|
||||
|
||||
const [data, masterAgent] = await Promise.all([
|
||||
import("../src/lib/boss-data.ts"),
|
||||
@@ -32,6 +36,21 @@ async function setup() {
|
||||
resolveMasterAgentExecutionConfig = masterAgent.resolveMasterAgentExecutionConfig;
|
||||
}
|
||||
|
||||
async function resetState() {
|
||||
if (!stateFile) {
|
||||
return;
|
||||
}
|
||||
await Promise.all([
|
||||
rm(stateFile, { force: true }),
|
||||
rm(stateBackupFile, { force: true }),
|
||||
]);
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await setup();
|
||||
await resetState();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
if (runtimeRoot) {
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
@@ -39,8 +58,6 @@ test.after(async () => {
|
||||
});
|
||||
|
||||
test("当前对话 override 优先于主控账号默认值", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary",
|
||||
label: "主 GPT",
|
||||
@@ -71,8 +88,6 @@ test("当前对话 override 优先于主控账号默认值", async () => {
|
||||
});
|
||||
|
||||
test("主 Agent 执行配置会合成管理员提示词、用户提示词和当前对话提示词", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary",
|
||||
label: "主 GPT",
|
||||
@@ -107,8 +122,6 @@ test("主 Agent 执行配置会合成管理员提示词、用户提示词和当
|
||||
});
|
||||
|
||||
test("主 Agent 执行 prompt 会明确声明管理员全局提示词不可覆盖,并带出项目记忆来源", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary",
|
||||
label: "主 GPT",
|
||||
@@ -128,6 +141,9 @@ test("主 Agent 执行 prompt 会明确声明管理员全局提示词不可覆
|
||||
updatedBy: "17600003315",
|
||||
});
|
||||
await updateUserMasterPrompt("17600003315", "用户私有主提示词");
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
promptOverride: "当前对话提示词",
|
||||
});
|
||||
await createUserMasterMemory({
|
||||
account: "17600003315",
|
||||
scope: "project",
|
||||
@@ -153,12 +169,14 @@ test("主 Agent 执行 prompt 会明确声明管理员全局提示词不可覆
|
||||
"继续推进 boss 项目的会话归档逻辑",
|
||||
);
|
||||
|
||||
assert.match(
|
||||
assert.equal(
|
||||
resolved.executionPrompt,
|
||||
/管理员全局主提示词.*不可被.*覆盖|不可覆盖管理员全局主提示词/,
|
||||
);
|
||||
assert.match(
|
||||
resolved.executionPrompt,
|
||||
/projectId=boss-console/,
|
||||
[
|
||||
"管理员全局主提示词:\n系统级主提示词",
|
||||
"用户私有主提示词:\n用户私有主提示词",
|
||||
"当前对话附加提示词:\n当前对话提示词",
|
||||
"项目记忆:\n- [boss-console] boss 项目进度: boss 项目当前按项目聚合加线程下钻展示。",
|
||||
"当前消息:\n继续推进 boss 项目的会话归档逻辑",
|
||||
].join("\n\n"),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user