From 384dd570de0a40353469c69e5ce18c67bc7bc0c1 Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 2 Apr 2026 22:32:19 +0800 Subject: [PATCH] refactor: extract execution prompt assembly --- .../2026-04-02-boss-execution-foundation.md | 7 + src/lib/boss-master-agent.ts | 186 ++++-------------- src/lib/execution/memory-resolver.ts | 88 +++++++++ src/lib/execution/prompt-assembler.ts | 48 +++++ tests/execution-memory-resolver.test.ts | 138 +++++++++++++ tests/execution-prompt-assembler.test.ts | 63 ++++++ tests/master-agent-config-resolution.test.ts | 42 ++-- 7 files changed, 415 insertions(+), 157 deletions(-) create mode 100644 src/lib/execution/memory-resolver.ts create mode 100644 src/lib/execution/prompt-assembler.ts create mode 100644 tests/execution-memory-resolver.test.ts create mode 100644 tests/execution-prompt-assembler.test.ts diff --git a/docs/superpowers/plans/2026-04-02-boss-execution-foundation.md b/docs/superpowers/plans/2026-04-02-boss-execution-foundation.md index a7dd37c..b50532a 100644 --- a/docs/superpowers/plans/2026-04-02-boss-execution-foundation.md +++ b/docs/superpowers/plans/2026-04-02-boss-execution-foundation.md @@ -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"; diff --git a/src/lib/boss-master-agent.ts b/src/lib/boss-master-agent.ts index 54e9ce9..14b5a8a 100644 --- a/src/lib/boss-master-agent.ts +++ b/src/lib/boss-master-agent.ts @@ -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>, - 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>, - 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>) { - return promptPolicy?.globalPrompt?.trim() - ? [`管理员全局主提示词:`, promptPolicy.globalPrompt.trim()].join("\n") - : "管理员全局主提示词:无"; -} - -function buildUserPromptDigest(userPrompt: Awaited>) { - return userPrompt?.content?.trim() - ? [`用户私有主提示词:`, userPrompt.content.trim()].join("\n") - : "用户私有主提示词:无"; -} - -function buildMemoryDigest(title: string, memories: Awaited>) { - 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>; - projectId: string; requestText: string; currentSessionExpiresAt?: string; agentControls?: ProjectAgentControls | null; - accountId: string; promptPolicy: Awaited>; userPrompt: Awaited>; - projectMemories: Awaited>; - userMemories: Awaited>; + 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>, 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>; userPrompt?: Awaited>; - projectMemories?: Awaited>; - userMemories?: Awaited>; + 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>; userPrompt?: Awaited>; - projectMemories?: Awaited>; - userMemories?: Awaited>; + 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 = { 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>, userPrompt?: Awaited>, - projectMemories?: Awaited>, - userMemories?: Awaited>, + 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>; userPrompt?: Awaited>; - projectMemories?: Awaited>; - userMemories?: Awaited>; + projectMemories?: RelevantMemory[]; + userMemories?: RelevantMemory[]; }) { const timer = setTimeout(() => { void (async () => { @@ -727,8 +625,8 @@ async function enqueueOpenAiMasterAgentReply(params: { agentControls?: ProjectAgentControls | null; promptPolicy?: Awaited>; userPrompt?: Awaited>; - projectMemories?: Awaited>; - userMemories?: Awaited>; + projectMemories?: RelevantMemory[]; + userMemories?: RelevantMemory[]; }) { const state = await readState(); const task = await queueMasterAgentTask({ @@ -866,16 +764,14 @@ function buildMasterCodexNodePrompt( agentControls?: ProjectAgentControls | null, promptPolicy?: Awaited>, userPrompt?: Awaited>, - projectMemories?: Awaited>, - userMemories?: Awaited>, + projectMemories?: RelevantMemory[], + userMemories?: RelevantMemory[], ) { return buildMasterAgentExecutionPrompt({ state, - projectId: "master-agent", requestText, currentSessionExpiresAt, agentControls, - accountId: "master-agent", promptPolicy: promptPolicy ?? null, userPrompt: userPrompt ?? null, projectMemories: projectMemories ?? [], diff --git a/src/lib/execution/memory-resolver.ts b/src/lib/execution/memory-resolver.ts new file mode 100644 index 0000000..c941bf7 --- /dev/null +++ b/src/lib/execution/memory-resolver.ts @@ -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; diff --git a/src/lib/execution/prompt-assembler.ts b/src/lib/execution/prompt-assembler.ts new file mode 100644 index 0000000..f545c6a --- /dev/null +++ b/src/lib/execution/prompt-assembler.ts @@ -0,0 +1,48 @@ +import type { MasterAgentMemory } from "@/lib/boss-data"; + +type PromptMemory = Pick; + +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"; diff --git a/tests/execution-memory-resolver.test.ts b/tests/execution-memory-resolver.test.ts new file mode 100644 index 0000000..05e0d94 --- /dev/null +++ b/tests/execution-memory-resolver.test.ts @@ -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"], + ); +}); diff --git a/tests/execution-prompt-assembler.test.ts b/tests/execution-prompt-assembler.test.ts new file mode 100644 index 0000000..0831dd7 --- /dev/null +++ b/tests/execution-prompt-assembler.test.ts @@ -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"); +}); diff --git a/tests/master-agent-config-resolution.test.ts b/tests/master-agent-config-resolution.test.ts index b339895..e771570 100644 --- a/tests/master-agent-config-resolution.test.ts +++ b/tests/master-agent-config-resolution.test.ts @@ -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"), ); });