feat(maf): conversation-driven MAF roundtable (R1/R2) + server-owned stage-3 dispatch
Backend:
- Conversation API at /api/maf-roundtable/conversations: create/list/detail,
paginated messages, atomic send+run start, owner-only isolation,
admin read-only diagnostic access
- SSE real-time event stream (GET /conversations/{id}/stream) backed by
ConversationEventHub + conversation_events store; multi-worker deployment doc
- Deterministic server-owned stage-3 dispatch contracts (dispatch_contracts.py):
routing resolved from trusted MAF deployment config instead of manager-chosen
agent ids; config.yaml keeps stage-3 routing deployment-owned
(default_manager_agent_id / seat ids / business_routes / max_seats_per_run)
- Conversation persistence (conversations/messages stores) + v2 migration script
- Tests: conversations, conversation stream, migration, phase3 conversation dispatch
Frontend:
- Vitest test infra for MAF (vitest.maf.config.ts + test:maf script) with
component tests for ApprovalCard/RunHeader/SessionsPage and use-maf-events
- Session form lib, flow-graph dialog / run-detail / sessions page updates,
routes and nav tweaks
- Docs: R0 legacy interaction baseline mapping, updated implementation plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
df408831e5
commit
99642fca6c
46 files changed
+6358
-1306
No files matched your search
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* MAF 会商创建表单的纯逻辑层。
|
||||
*
|
||||
* 把「从 health 默认值初始化表单」「校验最大轮数」「经理互斥过滤席位」「构建只含改写项的
|
||||
* 请求体」等逻辑抽成纯函数,作为组件与测试的共同事实源。组件不得在内部复制这些逻辑,
|
||||
* 测试必须 import 这里的真实函数,避免「实现坏掉、测试仍通过」。
|
||||
*/
|
||||
|
||||
export interface HealthDefaults {
|
||||
default_mode?: string;
|
||||
default_write_policy?: string;
|
||||
default_max_rounds?: number;
|
||||
}
|
||||
|
||||
export interface SessionFormState {
|
||||
title: string;
|
||||
taskText: string;
|
||||
mode: string;
|
||||
managerAgentId: string;
|
||||
seatAgentIds: string[];
|
||||
modelName: string;
|
||||
reasoningEffort: string;
|
||||
/** 文本输入,空串表示「使用默认」。 */
|
||||
maxRounds: string;
|
||||
writePolicy: string;
|
||||
showAdvanced: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 health 返回的后端默认值创建一份干净的初始表单。
|
||||
* - mode 默认取 health.default_mode(兜底 structured);
|
||||
* - writePolicy 默认取 health.default_write_policy(空串表示使用默认);
|
||||
* - maxRounds 默认取 health.default_max_rounds 的字符串(空串表示使用默认)。
|
||||
*
|
||||
* 注意:mode 即便来自 health 也只用作 UI 初始展示;提交时是否真的传回后端,
|
||||
* 由 buildSessionOverrides 按「是否被改写」决定(见下)。
|
||||
*/
|
||||
export function createInitialSessionForm(
|
||||
health: HealthDefaults | null | undefined,
|
||||
): SessionFormState {
|
||||
return {
|
||||
title: "",
|
||||
taskText: "",
|
||||
mode: health?.default_mode ?? "structured",
|
||||
managerAgentId: "",
|
||||
seatAgentIds: [],
|
||||
modelName: "",
|
||||
reasoningEffort: "",
|
||||
maxRounds:
|
||||
typeof health?.default_max_rounds === "number"
|
||||
? String(health.default_max_rounds)
|
||||
: "",
|
||||
writePolicy: health?.default_write_policy ?? "",
|
||||
showAdvanced: false,
|
||||
};
|
||||
}
|
||||
|
||||
export interface MaxRoundsValidation {
|
||||
valid: boolean;
|
||||
value?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验最大轮数输入。空串视为有效(= 使用后端默认)。必须是 1–64 的整数。
|
||||
*/
|
||||
export function validateMaxRounds(input: string): MaxRoundsValidation {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return { valid: true };
|
||||
const n = Number(trimmed);
|
||||
if (!Number.isInteger(n) || n < 1 || n > 64) {
|
||||
return { valid: false, error: "最大轮数必须为 1–64 的整数" };
|
||||
}
|
||||
return { valid: true, value: n };
|
||||
}
|
||||
|
||||
/**
|
||||
* 经理与席位互斥(后端参与者唯一约束):切换经理时,从已选席位中移除该 agent。
|
||||
*/
|
||||
export function filterSeatsForManager(seats: string[], managerId: string): string[] {
|
||||
if (!managerId) return seats;
|
||||
return seats.filter((id) => id !== managerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换某个席位的选择(toggle)。若该 agent 已是经理,则拒绝(返回原数组)。
|
||||
*/
|
||||
export function toggleSeat(
|
||||
seats: string[],
|
||||
agentId: string,
|
||||
managerAgentId: string,
|
||||
): string[] {
|
||||
if (agentId === managerAgentId) return seats;
|
||||
return seats.includes(agentId)
|
||||
? seats.filter((id) => id !== agentId)
|
||||
: [...seats, agentId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示一行只读的后端默认摘要(高级项折叠时用)。值只来自 health,绝不硬编码。
|
||||
*/
|
||||
export function defaultsSummaryLine(health: HealthDefaults | null | undefined): string {
|
||||
if (!health) return "正在读取后端默认配置…";
|
||||
return `当前后端默认:${health.default_mode} | ${health.default_write_policy} | 最大 ${health.default_max_rounds} 轮`;
|
||||
}
|
||||
|
||||
export interface SessionOverrides {
|
||||
mode?: "structured" | "group_chat";
|
||||
writePolicy?: "auto" | "confirm_high_risk" | "confirm_all_writes";
|
||||
maxRounds?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只产出「用户相对后端默认确实改写了」的字段。
|
||||
*
|
||||
* 设计:用户没改写 → 不传 → 后端决定(符合「最好不传对应字段」)。
|
||||
* 模式必须是 structured/group_chat 之一才算有效改写;写入策略必须是合法枚举值;
|
||||
* 最大轮数走 validateMaxRounds 校验。
|
||||
*/
|
||||
export function buildSessionOverrides(
|
||||
form: Pick<SessionFormState, "mode" | "writePolicy" | "maxRounds">,
|
||||
defaults: HealthDefaults | null | undefined,
|
||||
): { overrides: SessionOverrides; maxRoundsError?: string } {
|
||||
const result: SessionOverrides = {};
|
||||
let maxRoundsError: string | undefined;
|
||||
|
||||
// 模式:仅当与后端默认不同且合法时传
|
||||
if (form.mode !== (defaults?.default_mode ?? "structured")) {
|
||||
if (form.mode === "structured" || form.mode === "group_chat") {
|
||||
result.mode = form.mode;
|
||||
}
|
||||
}
|
||||
|
||||
// 写入策略:仅当与后端默认不同且合法时传
|
||||
const policyDefault = defaults?.default_write_policy ?? "";
|
||||
if (form.writePolicy !== policyDefault) {
|
||||
if (
|
||||
form.writePolicy === "auto" ||
|
||||
form.writePolicy === "confirm_high_risk" ||
|
||||
form.writePolicy === "confirm_all_writes"
|
||||
) {
|
||||
result.writePolicy = form.writePolicy;
|
||||
}
|
||||
// 空串 = 使用默认,不传
|
||||
}
|
||||
|
||||
// 最大轮数:空串不传;非空走校验
|
||||
const roundsValidation = validateMaxRounds(form.maxRounds);
|
||||
if (form.maxRounds.trim()) {
|
||||
if (!roundsValidation.valid) {
|
||||
maxRoundsError = roundsValidation.error;
|
||||
} else if (roundsValidation.value !== undefined) {
|
||||
const defaultRounds =
|
||||
typeof defaults?.default_max_rounds === "number"
|
||||
? defaults.default_max_rounds
|
||||
: null;
|
||||
if (defaultRounds === null || roundsValidation.value !== defaultRounds) {
|
||||
result.maxRounds = roundsValidation.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { overrides: result, maxRoundsError };
|
||||
}
|
||||
Reference in new issue
Block a user