feat: add master-agent prompts and memory management

This commit is contained in:
kris
2026-04-01 04:10:11 +08:00
parent 9000a9f185
commit d316f0490e
31 changed files with 4398 additions and 32 deletions

View File

@@ -218,6 +218,44 @@ export interface UserAttachmentStorageConfig {
validatedAt?: string;
}
export interface MasterAgentPromptPolicy {
globalPrompt: string;
updatedAt: string;
updatedBy?: string;
}
export interface UserMasterPrompt {
account: string;
content: string;
updatedAt: string;
}
export type MasterMemoryScope = "global" | "project";
export type MasterMemoryType =
| "user_preference"
| "project_progress"
| "decision"
| "risk"
| "blocking_issue"
| "research_note"
| "workflow_rule";
export interface MasterAgentMemory {
memoryId: string;
account: string;
scope: MasterMemoryScope;
projectId?: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags: string[];
sourceMessageId?: string;
createdAt: string;
updatedAt: string;
lastUsedAt?: string;
archived: boolean;
}
export interface GoalItem {
id: string;
text: string;
@@ -328,6 +366,7 @@ function buildCollaborationGate(project: Pick<Project, "isGroup" | "collaboratio
export interface ProjectAgentControls {
modelOverride?: string;
reasoningEffortOverride?: ReasoningEffort;
promptOverride?: string;
updatedAt: string;
}
@@ -809,6 +848,9 @@ export interface BossState {
deviceSkills: DeviceSkill[];
appLogs: AppLogEntry[];
userAttachmentStorageConfigs: UserAttachmentStorageConfig[];
masterAgentPromptPolicy: MasterAgentPromptPolicy | null;
userMasterPrompts: UserMasterPrompt[];
masterAgentMemories: MasterAgentMemory[];
threadContextSnapshots: ThreadContextSnapshot[];
threadHandoffPackages: ThreadHandoffPackage[];
threadContextAlerts: ThreadContextAlert[];
@@ -1223,6 +1265,9 @@ const initialState: BossState = {
updatedAt: nowIso(),
},
],
masterAgentPromptPolicy: null,
userMasterPrompts: [],
masterAgentMemories: [],
masterAgentTasks: [],
dispatchPlans: [],
dispatchExecutions: [],
@@ -2079,14 +2124,16 @@ function normalizeProjectAgentControls(
const reasoningEffortOverride = isReasoningEffort(raw?.reasoningEffortOverride)
? raw.reasoningEffortOverride
: undefined;
const promptOverride = trimToDefined(raw?.promptOverride);
if (!modelOverride && !reasoningEffortOverride) {
if (!modelOverride && !reasoningEffortOverride && !promptOverride) {
return undefined;
}
return {
modelOverride,
reasoningEffortOverride,
promptOverride,
updatedAt: raw?.updatedAt ?? nowIso(),
};
}
@@ -2512,6 +2559,67 @@ function normalizeAttachmentStorageConfig(
};
}
function normalizeMasterAgentPromptPolicy(
raw: Partial<MasterAgentPromptPolicy> | null | undefined,
fallback?: MasterAgentPromptPolicy | null,
): MasterAgentPromptPolicy | null {
if (!raw) {
return fallback ?? null;
}
const globalPrompt = raw.globalPrompt?.trim();
if (!globalPrompt) {
return fallback ?? null;
}
return {
globalPrompt,
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
updatedBy: raw.updatedBy?.trim() || fallback?.updatedBy,
};
}
function normalizeUserMasterPrompt(
raw: Partial<UserMasterPrompt>,
fallback?: UserMasterPrompt,
): UserMasterPrompt {
const account = raw.account ?? fallback?.account ?? "";
return {
account,
content: raw.content?.trim() ?? fallback?.content ?? "",
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
};
}
function normalizeMasterMemoryTags(values: string[] | undefined) {
return dedupeStrings(
(values ?? [])
.map((value) => value.trim())
.filter((value) => Boolean(value)),
);
}
function normalizeUserMasterMemory(
raw: Partial<MasterAgentMemory>,
fallback?: MasterAgentMemory,
): MasterAgentMemory {
const scope = raw.scope ?? fallback?.scope ?? "global";
const projectId = scope === "project" ? raw.projectId ?? fallback?.projectId : undefined;
return {
memoryId: raw.memoryId ?? fallback?.memoryId ?? randomToken("memory"),
account: raw.account ?? fallback?.account ?? "",
scope,
projectId,
title: raw.title?.trim() ?? fallback?.title ?? "",
content: raw.content?.trim() ?? fallback?.content ?? "",
memoryType: raw.memoryType ?? fallback?.memoryType ?? "user_preference",
tags: normalizeMasterMemoryTags(raw.tags ?? fallback?.tags ?? []),
sourceMessageId: raw.sourceMessageId ?? fallback?.sourceMessageId,
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
lastUsedAt: raw.lastUsedAt ?? fallback?.lastUsedAt,
archived: raw.archived ?? fallback?.archived ?? false,
};
}
function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
const base = fallback ?? cloneInitialState().projects[0];
const projectId = raw.id ?? base.id;
@@ -2745,6 +2853,24 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
base.userAttachmentStorageConfigs[index % base.userAttachmentStorageConfigs.length],
),
),
masterAgentPromptPolicy: normalizeMasterAgentPromptPolicy(
raw.masterAgentPromptPolicy,
base.masterAgentPromptPolicy,
),
userMasterPrompts: ensureArray(raw.userMasterPrompts, base.userMasterPrompts).map(
(prompt, index) =>
normalizeUserMasterPrompt(
prompt,
base.userMasterPrompts[index % Math.max(1, base.userMasterPrompts.length)],
),
),
masterAgentMemories: ensureArray(raw.masterAgentMemories, base.masterAgentMemories).map(
(memory, index) =>
normalizeUserMasterMemory(
memory,
base.masterAgentMemories[index % Math.max(1, base.masterAgentMemories.length)],
),
),
threadContextSnapshots: ensureArray(raw.threadContextSnapshots, base.threadContextSnapshots).map(
(snapshot, index) => ({
...base.threadContextSnapshots[index % base.threadContextSnapshots.length],
@@ -3410,6 +3536,7 @@ export async function updateProjectAgentControls(
payload: {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
promptOverride?: unknown;
},
) {
if (projectId !== "master-agent") {
@@ -3422,12 +3549,18 @@ export async function updateProjectAgentControls(
const reasoningEffortInput = Object.prototype.hasOwnProperty.call(payload, "reasoningEffortOverride")
? parseReasoningEffortOverride(payload.reasoningEffortOverride)
: { kind: "preserve" as const };
const promptOverrideInput = Object.prototype.hasOwnProperty.call(payload, "promptOverride")
? parseControlTextOverride(payload.promptOverride)
: { kind: "preserve" as const };
if (modelOverrideInput.kind === "invalid") {
throw new Error("INVALID_MODEL_OVERRIDE");
}
if (reasoningEffortInput.kind === "invalid") {
throw new Error("INVALID_REASONING_EFFORT_OVERRIDE");
}
if (promptOverrideInput.kind === "invalid") {
throw new Error("INVALID_PROMPT_OVERRIDE");
}
return mutateStateIfChanged((state) => {
const project = state.projects.find((item) => item.id === projectId);
@@ -3446,16 +3579,28 @@ export async function updateProjectAgentControls(
: reasoningEffortInput.kind === "clear"
? undefined
: currentControls?.reasoningEffortOverride;
const promptOverride =
promptOverrideInput.kind === "set"
? promptOverrideInput.value
: promptOverrideInput.kind === "clear"
? undefined
: currentControls?.promptOverride;
const currentModelOverride = currentControls?.modelOverride;
const currentReasoningEffortOverride = currentControls?.reasoningEffortOverride;
if (currentModelOverride === modelOverride && currentReasoningEffortOverride === reasoningEffortOverride) {
const currentPromptOverride = currentControls?.promptOverride;
if (
currentModelOverride === modelOverride &&
currentReasoningEffortOverride === reasoningEffortOverride &&
currentPromptOverride === promptOverride
) {
return { result: currentControls, changed: false };
}
const nextControls = {
modelOverride,
reasoningEffortOverride,
promptOverride,
updatedAt: nowIso(),
} satisfies ProjectAgentControls;
@@ -3496,6 +3641,423 @@ export async function upsertAttachmentStorageConfig(config: UserAttachmentStorag
});
}
export async function getMasterAgentPromptPolicy() {
const state = await readState();
return state.masterAgentPromptPolicy ?? null;
}
export async function updateMasterAgentPromptPolicy(input: {
globalPrompt: string;
updatedBy?: string;
}) {
const globalPrompt = input.globalPrompt.trim();
if (!globalPrompt) {
throw new Error("MASTER_AGENT_PROMPT_REQUIRED");
}
return mutateState((state) => {
const policy: MasterAgentPromptPolicy = {
globalPrompt,
updatedBy: input.updatedBy?.trim() || undefined,
updatedAt: nowIso(),
};
state.masterAgentPromptPolicy = policy;
return policy;
});
}
export async function getUserMasterPrompt(account: string) {
const state = await readState();
return state.userMasterPrompts.find((item) => item.account === account) ?? null;
}
export async function updateUserMasterPrompt(account: string, content: string) {
const trimmedContent = content.trim();
if (!trimmedContent) {
throw new Error("USER_MASTER_PROMPT_REQUIRED");
}
return mutateState((state) => {
const next: UserMasterPrompt = {
account,
content: trimmedContent,
updatedAt: nowIso(),
};
const existing = state.userMasterPrompts.find((item) => item.account === account);
if (existing) {
Object.assign(existing, next);
} else {
state.userMasterPrompts.unshift(next);
}
return next;
});
}
export async function clearUserMasterPrompt(account: string) {
return mutateState((state) => {
const before = state.userMasterPrompts.length;
state.userMasterPrompts = state.userMasterPrompts.filter((item) => item.account !== account);
return { cleared: before !== state.userMasterPrompts.length };
});
}
export async function listUserMasterMemories(
account: string,
options?: { includeArchived?: boolean; scope?: MasterMemoryScope; projectId?: string },
) {
const state = await readState();
const includeArchived = options?.includeArchived ?? false;
return [...state.masterAgentMemories]
.filter((memory) => {
if (memory.account !== account) return false;
if (!includeArchived && memory.archived) return false;
if (options?.scope && memory.scope !== options.scope) return false;
if (options?.projectId && memory.projectId !== options.projectId) return false;
return true;
})
.sort((a, b) => {
const timeDiff =
messageTimeValue(b.lastUsedAt ?? b.updatedAt ?? b.createdAt) -
messageTimeValue(a.lastUsedAt ?? a.updatedAt ?? a.createdAt);
if (timeDiff !== 0) return timeDiff;
return b.memoryId.localeCompare(a.memoryId);
});
}
export async function createUserMasterMemory(input: {
account: string;
scope: MasterMemoryScope;
projectId?: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags?: string[];
sourceMessageId?: string;
}) {
const title = input.title.trim();
const content = input.content.trim();
if (!title) {
throw new Error("USER_MASTER_MEMORY_TITLE_REQUIRED");
}
if (!content) {
throw new Error("USER_MASTER_MEMORY_CONTENT_REQUIRED");
}
if (input.scope === "project" && !input.projectId?.trim()) {
throw new Error("USER_MASTER_MEMORY_PROJECT_ID_REQUIRED");
}
return mutateState((state) => {
const now = nowIso();
const memory: MasterAgentMemory = {
memoryId: randomToken("memory"),
account: input.account,
scope: input.scope,
projectId: input.scope === "project" ? input.projectId?.trim() : undefined,
title,
content,
memoryType: input.memoryType,
tags: normalizeMasterMemoryTags(input.tags),
sourceMessageId: input.sourceMessageId,
createdAt: now,
updatedAt: now,
lastUsedAt: now,
archived: false,
};
state.masterAgentMemories.unshift(memory);
return memory;
});
}
export async function updateUserMasterMemory(
memoryId: string,
account: string,
patch: Partial<
Pick<
MasterAgentMemory,
"scope" | "projectId" | "title" | "content" | "memoryType" | "tags" | "sourceMessageId" | "lastUsedAt"
>
>,
) {
return mutateState((state) => {
const memory = state.masterAgentMemories.find(
(item) => item.memoryId === memoryId && item.account === account,
);
if (!memory) {
throw new Error("USER_MASTER_MEMORY_NOT_FOUND");
}
if (patch.scope) {
memory.scope = patch.scope;
}
if (memory.scope === "project" && patch.projectId !== undefined) {
memory.projectId = patch.projectId.trim() || undefined;
}
if (memory.scope !== "project") {
memory.projectId = undefined;
}
if (patch.title !== undefined) {
const title = patch.title.trim();
if (!title) throw new Error("USER_MASTER_MEMORY_TITLE_REQUIRED");
memory.title = title;
}
if (patch.content !== undefined) {
const content = patch.content.trim();
if (!content) throw new Error("USER_MASTER_MEMORY_CONTENT_REQUIRED");
memory.content = content;
}
if (patch.memoryType) {
memory.memoryType = patch.memoryType;
}
if (patch.tags) {
memory.tags = normalizeMasterMemoryTags(patch.tags);
}
if (patch.sourceMessageId !== undefined) {
memory.sourceMessageId = patch.sourceMessageId;
}
if (patch.lastUsedAt !== undefined) {
memory.lastUsedAt = patch.lastUsedAt;
}
memory.updatedAt = nowIso();
return memory;
});
}
export async function archiveUserMasterMemory(memoryId: string, account: string) {
return mutateState((state) => {
const memory = state.masterAgentMemories.find(
(item) => item.memoryId === memoryId && item.account === account,
);
if (!memory) {
throw new Error("USER_MASTER_MEMORY_NOT_FOUND");
}
memory.archived = true;
memory.updatedAt = nowIso();
return memory;
});
}
function normalizeAutoMemoryText(value: string | undefined) {
return (value ?? "")
.replace(/\s+/g, " ")
.replace(/[。;;!]+$/g, "")
.trim();
}
function inferAutoMemoryType(text: string): MasterMemoryType | null {
if (!text.trim()) return null;
if (/(微信|wechat|中文回复|中文沟通|UI风格|交互风格|偏好|习惯|默认)/i.test(text)) {
return "user_preference";
}
if (/(规则|约束|优先|先.*再|必须|不要|需要|流程|逻辑)/i.test(text)) {
return "workflow_rule";
}
if (/(阻塞|卡住|失败|异常|报错|问题|bug|未打通)/i.test(text)) {
return "blocking_issue";
}
if (/(风险|隐患|告警)/i.test(text)) {
return "risk";
}
if (/(决定|改成|采用|统一|确定|方案)/i.test(text)) {
return "decision";
}
if (/(调研|研究|结论)/i.test(text)) {
return "research_note";
}
if (/(进度|完成|已接通|已打通|上线|当前.*状态|回归|发布)/i.test(text)) {
return "project_progress";
}
return null;
}
function inferProjectAutoMemoryType(text: string): Exclude<MasterMemoryType, "user_preference"> | null {
if (!text.trim()) return null;
if (/(阻塞|卡住|失败|异常|报错|问题|bug|未打通)/i.test(text)) {
return "blocking_issue";
}
if (/(风险|隐患|告警)/i.test(text)) {
return "risk";
}
if (/(决定|改成|采用|统一|确定|方案)/i.test(text)) {
return "decision";
}
if (/(调研|研究|结论)/i.test(text)) {
return "research_note";
}
if (/(进度|完成|已接通|已打通|上线|当前.*状态|回归|发布)/i.test(text)) {
return "project_progress";
}
if (/(规则|约束|优先|先.*再|必须|不要|需要|流程|逻辑)/i.test(text)) {
return "workflow_rule";
}
return null;
}
function buildAutoMemoryTitle(memoryType: MasterMemoryType, label?: string) {
const typeLabel =
memoryType === "user_preference"
? "偏好"
: memoryType === "workflow_rule"
? "工作规则"
: memoryType === "blocking_issue"
? "阻塞"
: memoryType === "risk"
? "风险"
: memoryType === "decision"
? "决策"
: memoryType === "research_note"
? "调研结论"
: "项目进度";
return label ? `${label} · ${typeLabel}` : typeLabel;
}
function detectReferencedProjectForMemory(state: BossState, text: string) {
const lowered = text.toLowerCase();
const candidates = state.projects
.filter((project) => project.id !== "master-agent")
.flatMap((project) => {
const rawAliases = [
project.id,
project.name,
project.threadMeta.folderName,
project.threadMeta.threadDisplayName,
]
.map((value) => value.trim())
.filter(Boolean);
const aliases = Array.from(
new Set(
rawAliases.flatMap((alias) => {
const normalized = alias.trim();
if (!normalized) {
return [];
}
const tokenCandidates = normalized
.split(/[\s\-_/]+/)
.map((token) => token.trim())
.filter((token) => token.length >= 3);
return [normalized, ...tokenCandidates];
}),
),
);
return aliases.map((alias) => ({
projectId: project.id,
projectName: project.name,
alias,
}));
})
.sort((left, right) => right.alias.length - left.alias.length);
return candidates.find((candidate) => lowered.includes(candidate.alias.toLowerCase())) ?? null;
}
function upsertAutoMasterMemoryInState(
state: BossState,
input: {
account: string;
scope: MasterMemoryScope;
projectId?: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags: string[];
sourceMessageId?: string;
},
) {
const now = nowIso();
const existing = state.masterAgentMemories.find(
(memory) =>
memory.account === input.account &&
memory.scope === input.scope &&
(memory.projectId ?? undefined) === (input.projectId ?? undefined) &&
memory.title === input.title,
);
if (existing) {
existing.content = input.content;
existing.memoryType = input.memoryType;
existing.tags = normalizeMasterMemoryTags(input.tags);
existing.sourceMessageId = input.sourceMessageId ?? existing.sourceMessageId;
existing.archived = false;
existing.updatedAt = now;
existing.lastUsedAt = now;
return existing;
}
const memory: MasterAgentMemory = {
memoryId: randomToken("memory"),
account: input.account,
scope: input.scope,
projectId: input.scope === "project" ? input.projectId : undefined,
title: input.title,
content: input.content,
memoryType: input.memoryType,
tags: normalizeMasterMemoryTags(input.tags),
sourceMessageId: input.sourceMessageId,
createdAt: now,
updatedAt: now,
lastUsedAt: now,
archived: false,
};
state.masterAgentMemories.unshift(memory);
return memory;
}
function autoCaptureMasterAgentMemoriesInState(
state: BossState,
input: {
account: string;
requestText: string;
replyText: string;
sourceMessageId?: string;
},
) {
const requestText = normalizeAutoMemoryText(input.requestText);
const replyText = normalizeAutoMemoryText(input.replyText);
if (!requestText && !replyText) {
return [];
}
const createdOrUpdated: MasterAgentMemory[] = [];
const combined = [requestText, replyText].filter(Boolean).join(" ");
const preferenceType = inferAutoMemoryType(requestText);
if (preferenceType === "user_preference" || preferenceType === "workflow_rule") {
createdOrUpdated.push(
upsertAutoMasterMemoryInState(state, {
account: input.account,
scope: "global",
title: buildAutoMemoryTitle(preferenceType),
content: requestText,
memoryType: preferenceType,
tags: preferenceType === "user_preference" ? ["用户偏好"] : ["工作方式"],
sourceMessageId: input.sourceMessageId,
}),
);
}
const referencedProject = detectReferencedProjectForMemory(state, combined);
const projectType = inferProjectAutoMemoryType(replyText) ?? inferProjectAutoMemoryType(combined);
if (referencedProject && projectType) {
createdOrUpdated.push(
upsertAutoMasterMemoryInState(state, {
account: input.account,
scope: "project",
projectId: referencedProject.projectId,
title: buildAutoMemoryTitle(projectType, referencedProject.projectName),
content: replyText || requestText,
memoryType: projectType,
tags: [referencedProject.projectName, referencedProject.alias],
sourceMessageId: input.sourceMessageId,
}),
);
}
return createdOrUpdated;
}
function preferredDeviceForAccount(
state: BossState,
account: string,
@@ -5078,6 +5640,12 @@ export async function completeMasterAgentTask(payload: {
body: task.replyBody,
kind: "text",
});
autoCaptureMasterAgentMemoriesInState(state, {
account: task.requestedByAccount,
requestText: task.requestText,
replyText: task.replyBody,
sourceMessageId: task.requestMessageId,
});
}
} else if (!attachmentProjectId && payload.status === "failed") {
const isThreadConversationReply =