feat(ssa): Agent channel UX optimization (Solution B) + Plan-and-Execute architecture design
SSA Agent channel improvements (12 code files, +931/-203 lines): - Solution B: left/right separation of concerns (gaze guiding + state mutex + time-travel) - JWT token refresh mechanism (ensureFreshToken) to fix HTTP 401 during pipeline - Code truncation fix: LLM maxTokens 4000->8000 + CSS max-height 60vh - Retry streaming code generation with generateCodeStream() - R Docker structured errors: 20+ pattern matching + format_agent_error + line extraction - Prompt iron rules: strict output format in CoderAgent System Prompt - parseCode robustness: XML/Markdown/inference 3-tier matching + length validation - consoleOutput type defense: handle both array and scalar from R Docker unboxedJSON - Agent progress bar sync: derive phase from agentExecution.status - Export report / view code buttons restored for Agent mode - ExecutingProgress component: real-time timer + dynamic tips + step pulse animation Architecture design (3 review reports): - Plan-and-Execute step-by-step execution architecture approved - Code accumulation strategy (R Docker stays stateless) - 5 engineering guardrails: XML tags, AST pre-check, defensive prompts, high-fidelity schema, error classification circuit breaker Docs: update SSA module status v4.1, system status v6.7, deployment changelist Made-with: Cursor
This commit is contained in:
@@ -409,9 +409,34 @@ export class ChatHandlerService {
|
||||
userContent: string,
|
||||
writer: StreamWriter,
|
||||
placeholderMessageId: string,
|
||||
metadata?: Record<string, any>,
|
||||
): Promise<HandleResult> {
|
||||
try {
|
||||
// 1. 检查是否有等待确认的执行记录
|
||||
const agentAction = metadata?.agentAction as string | undefined;
|
||||
|
||||
// 1. 右侧工作区按钮操作(confirm_plan / confirm_code / cancel)
|
||||
if (agentAction) {
|
||||
const activeExec = await (prisma as any).ssaAgentExecution.findFirst({
|
||||
where: { sessionId, status: { in: ['plan_pending', 'code_pending'] } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (!activeExec) {
|
||||
return await this.handleAgentChat(sessionId, conversationId, writer, placeholderMessageId, null);
|
||||
}
|
||||
|
||||
if (agentAction === 'confirm_plan' && activeExec.status === 'plan_pending') {
|
||||
return await this.agentStreamCode(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
if (agentAction === 'confirm_code' && activeExec.status === 'code_pending') {
|
||||
return await this.agentExecuteCode(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
if (agentAction === 'cancel') {
|
||||
return await this.agentCancel(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否有等待确认的执行记录(用户在对话区直接打字确认)
|
||||
const activeExec = await (prisma as any).ssaAgentExecution.findFirst({
|
||||
where: { sessionId, status: { in: ['plan_pending', 'code_pending'] } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -419,27 +444,18 @@ export class ChatHandlerService {
|
||||
|
||||
if (activeExec) {
|
||||
const action = this.parseAgentAction(userContent);
|
||||
|
||||
if (activeExec.status === 'plan_pending') {
|
||||
if (action === 'confirm') {
|
||||
return await this.agentStreamCode(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
if (action === 'cancel') {
|
||||
return await this.agentCancel(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
if (activeExec.status === 'plan_pending' && action === 'confirm') {
|
||||
return await this.agentStreamCode(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
|
||||
if (activeExec.status === 'code_pending') {
|
||||
if (action === 'confirm') {
|
||||
return await this.agentExecuteCode(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
if (action === 'cancel') {
|
||||
return await this.agentCancel(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
if (activeExec.status === 'code_pending' && action === 'confirm') {
|
||||
return await this.agentExecuteCode(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
if (action === 'cancel') {
|
||||
return await this.agentCancel(activeExec, sessionId, conversationId, writer, placeholderMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 无挂起确认 — 检查是否是分析请求
|
||||
// 3. 无挂起确认 — 检查是否是分析请求
|
||||
const blackboard = await sessionBlackboardService.get(sessionId);
|
||||
const hasData = !!blackboard?.dataOverview;
|
||||
|
||||
@@ -455,10 +471,9 @@ export class ChatHandlerService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析用户回复中的确认/取消意图 */
|
||||
private parseAgentAction(content: string): 'confirm' | 'cancel' | 'other' {
|
||||
const lc = content.toLowerCase();
|
||||
if (lc.includes('agent_confirm') || lc.includes('确认') || lc.includes('执行代码') || lc.includes('开始生成')) return 'confirm';
|
||||
if (lc.includes('agent_confirm') || lc.includes('确认') || lc.includes('执行代码') || lc.includes('开始生成') || lc.includes('执行')) return 'confirm';
|
||||
if (lc.includes('agent_cancel') || lc.includes('取消')) return 'cancel';
|
||||
return 'other';
|
||||
}
|
||||
@@ -501,26 +516,18 @@ export class ChatHandlerService {
|
||||
planText: plan.rawText,
|
||||
});
|
||||
|
||||
// 流式解释计划
|
||||
// 简短视线牵引语(方案 B:左侧只输出简短提示,详细内容在右侧工作区)
|
||||
const hint = [
|
||||
`[系统指令] 你刚刚制定了分析计划「${plan.title}」,包含 ${plan.steps.length} 个步骤。`,
|
||||
'请用简洁的自然语言解释这个计划,然后告知用户可以确认后开始生成 R 代码。',
|
||||
'【禁止】不要编造数值或结果。',
|
||||
`[系统指令——严格遵守] 分析计划已生成(${plan.steps.length} 步),标题「${plan.title}」。`,
|
||||
'你只需回复 1-2 句话,引导用户去右侧工作区查看。',
|
||||
'示例回复:「我已为您拟定了详细的分析计划,请在右侧工作区核对并点击确认。」',
|
||||
'【铁律】禁止在对话中重复计划内容、禁止编造任何数值。回复不超过 2 句话。',
|
||||
].join('\n');
|
||||
|
||||
const msgs = await conversationService.buildContext(sessionId, conversationId, 'analyze', hint);
|
||||
const sr = await conversationService.streamToSSE(msgs, writer, { temperature: 0.5, maxTokens: 800 });
|
||||
const sr = await conversationService.streamToSSE(msgs, writer, { temperature: 0.3, maxTokens: 150 });
|
||||
await conversationService.finalizeAssistantMessage(placeholderMessageId, sr.content, sr.thinking, sr.tokens);
|
||||
|
||||
sendEvent('ask_user', {
|
||||
questionId: `agent_plan_${execution.id}`,
|
||||
text: '请确认分析计划',
|
||||
options: [
|
||||
{ id: 'agent_confirm_plan', label: '确认方案,生成代码' },
|
||||
{ id: 'agent_cancel', label: '取消' },
|
||||
],
|
||||
});
|
||||
|
||||
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
||||
}
|
||||
|
||||
@@ -564,26 +571,18 @@ export class ChatHandlerService {
|
||||
explanation: generated.explanation,
|
||||
});
|
||||
|
||||
// 流式说明代码
|
||||
// 简短视线牵引语
|
||||
const hint = [
|
||||
`[系统指令] R 代码已生成(${generated.code.split('\n').length} 行),使用 ${generated.requiredPackages.join(', ') || '基础包'}。`,
|
||||
'请简要说明代码逻辑,告知用户可以确认执行。',
|
||||
'【禁止】不要在对话中贴代码(工作区已展示),不要编造结果。',
|
||||
`[系统指令——严格遵守] R 代码已生成(${generated.code.split('\n').length} 行),使用 ${generated.requiredPackages.join(', ') || '基础包'}。`,
|
||||
'你只需回复 1-2 句话,引导用户去右侧工作区核对代码并点击执行。',
|
||||
'示例回复:「R 代码已生成,请在右侧工作区核对代码并点击 "确认并执行"。」',
|
||||
'【铁律】禁止在对话中贴代码、禁止编造结果。回复不超过 2 句话。',
|
||||
].join('\n');
|
||||
|
||||
const msgs = await conversationService.buildContext(sessionId, conversationId, 'analyze', hint);
|
||||
const sr = await conversationService.streamToSSE(msgs, writer, { temperature: 0.5, maxTokens: 600 });
|
||||
const sr = await conversationService.streamToSSE(msgs, writer, { temperature: 0.3, maxTokens: 150 });
|
||||
await conversationService.finalizeAssistantMessage(placeholderMessageId, sr.content, sr.thinking, sr.tokens);
|
||||
|
||||
sendEvent('ask_user', {
|
||||
questionId: `agent_code_${execution.id}`,
|
||||
text: '代码已生成,请确认执行',
|
||||
options: [
|
||||
{ id: 'agent_confirm_code', label: '执行代码' },
|
||||
{ id: 'agent_cancel', label: '取消' },
|
||||
],
|
||||
});
|
||||
|
||||
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
||||
}
|
||||
|
||||
@@ -655,12 +654,42 @@ export class ChatHandlerService {
|
||||
}
|
||||
|
||||
lastError = execResult.error || '执行失败';
|
||||
const rawConsole = execResult.consoleOutput;
|
||||
const consoleArr = Array.isArray(rawConsole) ? rawConsole : (rawConsole ? [String(rawConsole)] : []);
|
||||
const consoleSnippet = consoleArr.slice(-20).join('\n');
|
||||
|
||||
if (attempt < codeRunnerService.maxRetries) {
|
||||
sendEvent('code_error', { executionId: execution.id, message: lastError, willRetry: true });
|
||||
sendEvent('code_retry', { executionId: execution.id, retryCount: attempt + 1, message: `错误: ${lastError},正在修复代码...` });
|
||||
const errorDetail = consoleSnippet
|
||||
? `${lastError}\n\n--- R console output (last 20 lines) ---\n${consoleSnippet}`
|
||||
: lastError;
|
||||
|
||||
const retry = await agentCoderService.generateCode(sessionId, plan, lastError);
|
||||
sendEvent('code_error', {
|
||||
executionId: execution.id,
|
||||
message: lastError,
|
||||
consoleOutput: consoleSnippet || undefined,
|
||||
willRetry: true,
|
||||
retryCount: attempt + 1,
|
||||
});
|
||||
|
||||
sendEvent('code_retry', {
|
||||
executionId: execution.id,
|
||||
retryCount: attempt + 1,
|
||||
message: `第 ${attempt + 1} 次执行失败,Agent 正在重新生成代码...`,
|
||||
previousError: lastError,
|
||||
});
|
||||
|
||||
const retry = await agentCoderService.generateCodeStream(
|
||||
sessionId,
|
||||
plan,
|
||||
(accumulated) => {
|
||||
sendEvent('code_generating', {
|
||||
executionId: execution.id,
|
||||
partialCode: accumulated,
|
||||
});
|
||||
},
|
||||
errorDetail,
|
||||
currentCode,
|
||||
);
|
||||
currentCode = retry.code;
|
||||
|
||||
await (prisma as any).ssaAgentExecution.update({
|
||||
|
||||
Reference in New Issue
Block a user