feat(ssa): Implement dual-channel architecture Phase 1-3 (QPER + LLM Agent pipeline)

Completed:
- Phase 1: DB schema (execution_mode + ssa_agent_executions), ModeToggle component, Session PATCH API
- Phase 2: AgentPlannerService + AgentCoderService (streaming) + CodeRunnerService + R Docker /execute-code endpoint
- Phase 3: AgentCodePanel (3-step confirmation UI), SSE event handling (7 agent events), streaming code display
- Three-step confirmation pipeline: plan -> user confirm -> stream code -> user confirm -> execute R code -> results
- R Docker sandbox /execute-code endpoint with 120s timeout + block_helpers preloaded
- E2E dual-channel test script (8 tests)
- Updated R engine architecture doc (v1.5) and SSA module status doc (v4.0)

Technical details:
- AgentCoderService uses LLM streaming (chatStream) for real-time code generation feedback
- ReviewerAgent temporarily disabled, prioritizing Plan -> Code -> Execute flow
- CodeRunnerService wraps user code with auto data loading (df variable injection)
- Frontend handles agent_planning, agent_plan_ready, code_generating, code_generated, code_executing, code_result events
- ask_user mechanism used for plan and code confirmation steps

Files: 24 files (4 new services, 2 new components, 1 migration, 1 E2E test, 16 modified)
Made-with: Cursor
This commit is contained in:
2026-03-02 22:23:54 +08:00
parent 71d32d11ee
commit aadceb5cde
24 changed files with 2694 additions and 56 deletions

View File

@@ -969,9 +969,7 @@
.message-bubble .markdown-content h2:first-child,
.message-bubble .markdown-content h3:first-child {
margin-top: 0;
}
.message-bubble .markdown-content h1 {
}.message-bubble .markdown-content h1 {
font-size: 1.3em;
}.message-bubble .markdown-content h2 {
font-size: 1.2em;
@@ -1004,4 +1002,4 @@
border-radius: 4px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
}
}

View File

@@ -0,0 +1,164 @@
/**
* AgentCodePanel — Agent 通道工作区面板
*
* 分步展示:计划 → 流式代码生成 → 执行结果
* 在 Agent 模式下替代 WorkflowTimeline。
*/
import React from 'react';
import {
Code,
CheckCircle,
XCircle,
Loader2,
RefreshCw,
Sparkles,
FileText,
Play,
} from 'lucide-react';
import { useSSAStore } from '../stores/ssaStore';
import type { AgentExecutionStatus } from '../types';
const STATUS_LABEL: Record<AgentExecutionStatus, string> = {
pending: '等待中',
planning: '制定计划中...',
plan_pending: '等待确认计划',
coding: '生成 R 代码中...',
code_pending: '等待确认执行',
executing: '执行 R 代码中...',
completed: '执行完成',
error: '执行出错',
};
const StatusBadge: React.FC<{ status: AgentExecutionStatus }> = ({ status }) => {
const spinning = ['planning', 'coding', 'executing'].includes(status);
const waiting = ['plan_pending', 'code_pending'].includes(status);
return (
<span className={`agent-code-status ${status}`}>
{spinning && <Loader2 size={12} className="spin" />}
{waiting && <Play size={12} />}
{status === 'completed' && <CheckCircle size={12} />}
{status === 'error' && <XCircle size={12} />}
{STATUS_LABEL[status]}
</span>
);
};
export const AgentCodePanel: React.FC = () => {
const { agentExecution, executionMode } = useSSAStore();
if (executionMode !== 'agent') return null;
if (!agentExecution) {
return (
<div className="agent-code-panel">
<div className="agent-code-header">
<h4><Sparkles size={14} /> Agent </h4>
</div>
<div className="agent-code-empty">
<Sparkles size={32} style={{ color: '#94a3b8', marginBottom: 12 }} />
<p></p>
<p>Agent R </p>
</div>
</div>
);
}
const { status, planText, planSteps, generatedCode, partialCode, errorMessage, retryCount, durationMs } = agentExecution;
const displayCode = generatedCode || partialCode;
const isStreamingCode = status === 'coding' && !!partialCode && !generatedCode;
return (
<div className="agent-code-panel">
<div className="agent-code-header">
<h4><Code size={14} /> Agent 线</h4>
<StatusBadge status={status} />
</div>
{/* Step 1: 分析计划 */}
{(planText || planSteps) && (
<div className="agent-section">
<div className="agent-section-title">
<FileText size={13} />
<span></span>
{status === 'plan_pending' && <span className="badge-waiting"></span>}
{['coding', 'code_pending', 'executing', 'completed'].includes(status) && (
<span className="badge-done"></span>
)}
</div>
{planSteps && planSteps.length > 0 && (
<div className="agent-plan-steps">
{planSteps.map((s, i) => (
<div key={i} className="plan-step-item">
<span className="step-num">{s.order}</span>
<span className="step-method">{s.method}</span>
<span className="step-desc">{s.description}</span>
</div>
))}
</div>
)}
{planText && !planSteps?.length && (
<div className="agent-plan-text">{planText}</div>
)}
</div>
)}
{/* Step 2: R 代码 */}
{(displayCode || status === 'coding') && (
<div className="agent-section">
<div className="agent-section-title">
<Code size={13} />
<span>R </span>
{isStreamingCode && <span className="badge-streaming"><Loader2 size={11} className="spin" /> ...</span>}
{status === 'code_pending' && <span className="badge-waiting"></span>}
{['executing', 'completed'].includes(status) && <span className="badge-done"></span>}
</div>
<div className="agent-code-body">
{displayCode ? (
<pre className={isStreamingCode ? 'streaming' : ''}>{displayCode}</pre>
) : (
<div className="agent-code-loading">
<Loader2 size={16} className="spin" />
<span>Agent R ...</span>
</div>
)}
</div>
</div>
)}
{/* 重试信息 */}
{retryCount > 0 && (
<div className="agent-retry-info">
<RefreshCw size={14} />
<span> {retryCount} Agent </span>
</div>
)}
{/* 错误信息 */}
{status === 'error' && errorMessage && errorMessage !== '用户取消' && (
<div className="agent-error-bar">
<XCircle size={13} />
<pre>{errorMessage}</pre>
</div>
)}
{/* 执行中 */}
{status === 'executing' && (
<div className="agent-executing-bar">
<Loader2 size={14} className="spin" />
<span>R ...</span>
</div>
)}
{/* 完成状态 */}
{status === 'completed' && durationMs != null && (
<div className="agent-success-bar">
<CheckCircle size={13} />
<span> {(durationMs / 1000).toFixed(1)}s</span>
</div>
)}
</div>
);
};
export default AgentCodePanel;

View File

@@ -0,0 +1,74 @@
/**
* ModeToggle — 双通道切换组件
*
* 在 QPER 管线(预制工具)与 Agent 通道LLM 代码生成)之间切换。
* 切换时调用后端 PATCH API 更新 session.executionMode。
*/
import React, { useState, useCallback } from 'react';
import { Cpu, Sparkles, Loader2 } from 'lucide-react';
import { useSSAStore } from '../stores/ssaStore';
import type { ExecutionMode } from '../types';
import apiClient from '@/common/api/axios';
export const ModeToggle: React.FC = () => {
const {
executionMode,
setExecutionMode,
currentSession,
isExecuting,
addToast,
} = useSSAStore();
const [isSwitching, setIsSwitching] = useState(false);
const handleToggle = useCallback(async () => {
if (!currentSession || isExecuting || isSwitching) return;
const nextMode: ExecutionMode = executionMode === 'qper' ? 'agent' : 'qper';
try {
setIsSwitching(true);
await apiClient.patch(`/api/v1/ssa/sessions/${currentSession.id}/execution-mode`, {
executionMode: nextMode,
});
setExecutionMode(nextMode);
addToast(
nextMode === 'agent'
? '已切换到 Agent 通道LLM 代码生成)'
: '已切换到 QPER 管线(预制工具)',
'info',
);
} catch (err: any) {
addToast(err?.message || '切换失败', 'error');
} finally {
setIsSwitching(false);
}
}, [currentSession, executionMode, isExecuting, isSwitching, setExecutionMode, addToast]);
if (!currentSession) return null;
return (
<div className="ssa-mode-toggle">
<button
className={`mode-toggle-btn ${executionMode === 'qper' ? 'active' : ''}`}
onClick={() => executionMode !== 'qper' && handleToggle()}
disabled={isSwitching || isExecuting}
title="QPER 管线:预制统计工具,结构化流程"
>
<Cpu size={14} />
<span>QPER</span>
</button>
<button
className={`mode-toggle-btn ${executionMode === 'agent' ? 'active' : ''}`}
onClick={() => executionMode !== 'agent' && handleToggle()}
disabled={isSwitching || isExecuting}
title="Agent 通道LLM 生成 R 代码,灵活分析"
>
{isSwitching ? <Loader2 size={14} className="spin" /> : <Sparkles size={14} />}
<span>Agent</span>
</button>
</div>
);
};
export default ModeToggle;

View File

@@ -43,6 +43,7 @@ import { ClarificationCard } from './ClarificationCard';
import { AskUserCard } from './AskUserCard';
import type { AskUserResponseData } from './AskUserCard';
import { ThinkingBlock } from '@/shared/components/Chat';
import { ModeToggle } from './ModeToggle';
import type { ClarificationCardData, IntentResult } from '../types';
export const SSAChatPane: React.FC = () => {
@@ -285,14 +286,17 @@ export const SSAChatPane: React.FC = () => {
{currentSession?.title || '新的统计分析'}
</span>
</div>
<EngineStatus
isExecuting={isExecuting}
isLoading={isLoading || isPlanLoading}
isUploading={uploadStatus === 'uploading' || uploadStatus === 'parsing'}
isProfileLoading={isProfileLoading || dataProfileLoading}
isStreaming={isGenerating}
streamIntent={currentIntent}
/>
<div className="chat-header-right">
{currentSession && <ModeToggle />}
<EngineStatus
isExecuting={isExecuting}
isLoading={isLoading || isPlanLoading}
isUploading={uploadStatus === 'uploading' || uploadStatus === 'parsing'}
isProfileLoading={isProfileLoading || dataProfileLoading}
isStreaming={isGenerating}
streamIntent={currentIntent}
/>
</div>
</header>
{/* Chat Messages */}

View File

@@ -27,6 +27,7 @@ import type { AnalysisRecord } from '../stores/ssaStore';
import { useWorkflow } from '../hooks/useWorkflow';
import type { TraceStep, ReportBlock, WorkflowStepResult } from '../types';
import { WorkflowTimeline, detectPlanMismatches } from './WorkflowTimeline';
import { AgentCodePanel } from './AgentCodePanel';
import { DynamicReport } from './DynamicReport';
import { exportBlocksToWord } from '../utils/exportBlocksToWord';
import apiClient from '@/common/api/axios';
@@ -48,6 +49,8 @@ export const SSAWorkspacePane: React.FC = () => {
hasUnsavedPlanChanges,
setHasUnsavedPlanChanges,
dataContext,
executionMode,
agentExecution,
} = useSSAStore();
const { executeWorkflow, cancelWorkflow, isExecuting: isWorkflowExecuting } = useWorkflow();
@@ -299,8 +302,29 @@ export const SSAWorkspacePane: React.FC = () => {
{/* Canvas — single scroll */}
<div className="workspace-canvas" ref={containerRef}>
<div className="workspace-scroll-container">
{/* ===== Agent 模式工作区 ===== */}
{executionMode === 'agent' && (
<div className="section-block">
<AgentCodePanel />
{/* Agent 模式的报告输出复用 DynamicReport */}
{agentExecution?.status === 'completed' && agentExecution.reportBlocks && agentExecution.reportBlocks.length > 0 && (
<div className="section-block result-section" ref={resultRef}>
<div className="section-divider">
<span className="divider-line" />
<span className="divider-label"></span>
<span className="divider-line" />
</div>
<div className="view-result fade-in">
<DynamicReport blocks={agentExecution.reportBlocks} />
</div>
</div>
)}
</div>
)}
{/* ===== QPER 模式工作区 ===== */}
{/* Empty state */}
{!plan && (
{executionMode === 'qper' && !plan && (
<div className="view-empty fade-in">
<div className="empty-icon">
<FileQuestion size={48} />
@@ -314,8 +338,8 @@ export const SSAWorkspacePane: React.FC = () => {
</div>
)}
{/* ===== Block 1: SAP ===== */}
{plan && (
{/* ===== Block 1: SAP (QPER only) ===== */}
{executionMode === 'qper' && plan && (
<div className="section-block sap-section-block">
<div className="section-divider">
<span className="divider-line" />
@@ -372,8 +396,8 @@ export const SSAWorkspacePane: React.FC = () => {
</div>
)}
{/* ===== Block 2: Execution log ===== */}
{steps.length > 0 && (
{/* ===== Block 2: Execution log (QPER only) ===== */}
{executionMode === 'qper' && steps.length > 0 && (
<div className="section-block execution-section" ref={executionRef}>
<div className="section-divider">
<span className="divider-line" />
@@ -450,8 +474,8 @@ export const SSAWorkspacePane: React.FC = () => {
</div>
)}
{/* ===== Block 3: Results ===== */}
{hasResults && (
{/* ===== Block 3: Results (QPER only) ===== */}
{executionMode === 'qper' && hasResults && (
<div className="section-block result-section" ref={resultRef}>
<div className="section-divider">
<span className="divider-line" />

View File

@@ -257,6 +257,88 @@ export function useSSAChat(): UseSSAChatReturn {
continue;
}
// ── Agent 通道 SSE 事件 ──
if (parsed.type === 'agent_planning') {
const { setAgentExecution, setWorkspaceOpen, setActivePane } = useSSAStore.getState();
setAgentExecution({
id: parsed.executionId || crypto.randomUUID(),
sessionId: '',
query: content,
retryCount: 0,
status: 'planning',
});
setActivePane('execution');
setWorkspaceOpen(true);
continue;
}
if (parsed.type === 'agent_plan_ready') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({
planText: parsed.planText,
planSteps: parsed.plan?.steps,
status: 'plan_pending',
});
continue;
}
if (parsed.type === 'code_generating') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({
partialCode: parsed.partialCode,
status: 'coding',
});
continue;
}
if (parsed.type === 'code_generated') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({
generatedCode: parsed.code,
partialCode: undefined,
status: parsed.code ? 'code_pending' : 'coding',
});
continue;
}
if (parsed.type === 'code_executing') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({ status: 'executing' });
continue;
}
if (parsed.type === 'code_result') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({
reportBlocks: parsed.reportBlocks,
generatedCode: parsed.code || useSSAStore.getState().agentExecution?.generatedCode,
status: 'completed',
durationMs: parsed.durationMs,
});
continue;
}
if (parsed.type === 'code_error') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({
status: 'error',
errorMessage: parsed.message,
});
continue;
}
if (parsed.type === 'code_retry') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({
status: 'coding',
retryCount: parsed.retryCount || 0,
generatedCode: parsed.code,
errorMessage: undefined,
});
continue;
}
// 错误事件
if (parsed.type === 'error') {
const errMsg = parsed.message || '处理消息时发生错误';

View File

@@ -8,6 +8,7 @@
import { create } from 'zustand';
import type {
SSAMode,
ExecutionMode,
SSASession,
SSAMessage,
DataProfile,
@@ -18,6 +19,7 @@ import type {
VariableDictEntry,
FiveSectionReport,
VariableDetailData,
AgentExecutionRecord,
} from '../types';
type ArtifactPane = 'empty' | 'sap' | 'execution' | 'result';
@@ -73,8 +75,16 @@ interface SSAState {
dataProfileModalVisible: boolean;
workflowPlanLoading: boolean;
/** 双通道执行模式 */
executionMode: ExecutionMode;
/** Agent 通道当前执行记录 */
agentExecution: AgentExecutionRecord | null;
// ---- actions ----
setMode: (mode: SSAMode) => void;
setExecutionMode: (mode: ExecutionMode) => void;
setAgentExecution: (exec: AgentExecutionRecord | null) => void;
updateAgentExecution: (patch: Partial<AgentExecutionRecord>) => void;
setCurrentSession: (session: SSASession | null) => void;
addMessage: (message: SSAMessage) => void;
setMessages: (messages: SSAMessage[]) => void;
@@ -141,6 +151,8 @@ const initialState = {
dataProfileModalVisible: false,
workflowPlanLoading: false,
hasUnsavedPlanChanges: false,
executionMode: 'qper' as ExecutionMode,
agentExecution: null as AgentExecutionRecord | null,
dataContext: {
dataOverview: null,
variableDictionary: [],
@@ -157,6 +169,12 @@ export const useSSAStore = create<SSAState>((set) => ({
...initialState,
setMode: (mode) => set({ mode }),
setExecutionMode: (mode) => set({ executionMode: mode }),
setAgentExecution: (exec) => set({ agentExecution: exec }),
updateAgentExecution: (patch) =>
set((state) => ({
agentExecution: state.agentExecution ? { ...state.agentExecution, ...patch } : null,
})),
setCurrentSession: (session) => set({ currentSession: session }),
addMessage: (message) =>
set((state) => ({ messages: [...state.messages, message] })),

View File

@@ -340,6 +340,12 @@
gap: 16px;
}
.chat-header-right {
display: flex;
align-items: center;
gap: 12px;
}
.back-btn {
display: flex;
align-items: center;

View File

@@ -1220,3 +1220,281 @@
font-weight: 500;
}
.wt-mismatch-force:hover { background: #fee2e2; }
/* ========== ModeToggle — 双通道切换 ========== */
.ssa-mode-toggle {
display: inline-flex;
background: #f1f5f9;
border-radius: 8px;
padding: 2px;
gap: 2px;
}
.mode-toggle-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
border: none;
border-radius: 6px;
background: transparent;
font-size: 12px;
font-weight: 500;
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.mode-toggle-btn:hover:not(:disabled):not(.active) {
color: #334155;
background: #e2e8f0;
}
.mode-toggle-btn.active {
background: #fff;
color: #1e40af;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.mode-toggle-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ========== AgentCodePanel — Agent 代码面板 ========== */
/* ── Agent Code Panel ── */
.agent-code-panel {
background: #1e293b;
border-radius: 10px;
overflow: hidden;
margin: 12px 0;
}
.agent-code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #0f172a;
border-bottom: 1px solid #334155;
}
.agent-code-header h4 {
margin: 0;
font-size: 13px;
font-weight: 600;
color: #94a3b8;
display: flex;
align-items: center;
gap: 6px;
}
.agent-code-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
padding: 3px 10px;
border-radius: 12px;
font-weight: 500;
}
.agent-code-status.planning { background: #1e3a5f; color: #60a5fa; }
.agent-code-status.plan_pending { background: #3b2f1e; color: #fbbf24; }
.agent-code-status.coding { background: #1e3a5f; color: #60a5fa; }
.agent-code-status.code_pending { background: #3b2f1e; color: #fbbf24; }
.agent-code-status.executing { background: #1e3a5f; color: #60a5fa; }
.agent-code-status.completed { background: #1a3a2a; color: #4ade80; }
.agent-code-status.error { background: #3b1e1e; color: #f87171; }
/* Agent 分步区块 */
.agent-section {
border-bottom: 1px solid #334155;
}
.agent-section:last-child {
border-bottom: none;
}
.agent-section-title {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px 6px;
font-size: 12px;
font-weight: 600;
color: #94a3b8;
}
.badge-waiting {
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
background: #3b2f1e;
color: #fbbf24;
font-weight: 500;
}
.badge-done {
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
background: #1a3a2a;
color: #4ade80;
font-weight: 500;
}
.badge-streaming {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
background: #1e3a5f;
color: #60a5fa;
font-weight: 500;
}
/* 计划步骤列表 */
.agent-plan-steps {
padding: 4px 16px 12px;
}
.plan-step-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 5px 0;
font-size: 12px;
color: #cbd5e1;
line-height: 1.5;
}
.plan-step-item .step-num {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #334155;
color: #94a3b8;
font-size: 11px;
font-weight: 600;
}
.plan-step-item .step-method {
flex-shrink: 0;
font-weight: 600;
color: #60a5fa;
min-width: 60px;
}
.plan-step-item .step-desc {
color: #94a3b8;
}
.agent-plan-text {
padding: 4px 16px 12px;
font-size: 13px;
color: #cbd5e1;
line-height: 1.6;
white-space: pre-wrap;
}
/* 代码区域 */
.agent-code-body {
padding: 12px 16px;
max-height: 400px;
overflow-y: auto;
}
.agent-code-body pre {
margin: 0;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
white-space: pre-wrap;
word-break: break-all;
}
.agent-code-body pre.streaming {
border-right: 2px solid #60a5fa;
animation: cursor-blink 1s step-end infinite;
}
@keyframes cursor-blink {
50% { border-color: transparent; }
}
.agent-code-loading {
display: flex;
align-items: center;
gap: 8px;
color: #64748b;
font-size: 13px;
padding: 16px 0;
}
.agent-code-empty {
color: #64748b;
font-size: 13px;
text-align: center;
padding: 32px 16px;
}
/* 状态条 */
.agent-retry-info {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: #3b2f1e;
border-top: 1px solid #534120;
font-size: 12px;
color: #fbbf24;
}
.agent-error-bar {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 16px;
background: #3b1e1e;
border-top: 1px solid #7f1d1d;
font-size: 12px;
color: #fca5a5;
}
.agent-error-bar pre {
margin: 0;
font-size: 12px;
white-space: pre-wrap;
color: #fecaca;
}
.agent-executing-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: #1e3a5f;
border-top: 1px solid #1e40af;
font-size: 12px;
color: #93c5fd;
}
.agent-success-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #1a3a2a;
border-top: 1px solid #166534;
font-size: 12px;
color: #86efac;
}

View File

@@ -4,6 +4,9 @@
export type SSAMode = 'analysis' | 'consult';
/** 双通道执行模式QPER 管线 / LLM Agent 代码生成 */
export type ExecutionMode = 'qper' | 'agent';
export type SessionStatus = 'active' | 'completed' | 'archived';
export interface SSASession {
@@ -367,12 +370,36 @@ export interface ClarificationOptionData {
description?: string;
}
/** SSE 消息类型Phase R 扩展qper_status + reflection_complete */
/** SSE 消息类型Phase R 扩展qper_status + reflection_complete + Agent 通道事件 */
export type SSEMessageType =
| 'connected'
| 'step_start' | 'step_progress' | 'step_complete' | 'step_error'
| 'workflow_complete' | 'workflow_error'
| 'qper_status' | 'reflection_complete';
| 'qper_status' | 'reflection_complete'
| 'agent_planning' | 'agent_plan_ready'
| 'code_generating' | 'code_generated'
| 'code_executing' | 'code_result' | 'code_error' | 'code_retry';
/** Agent 通道执行状态 */
export type AgentExecutionStatus =
| 'pending' | 'planning' | 'plan_pending'
| 'coding' | 'code_pending'
| 'executing' | 'completed' | 'error';
export interface AgentExecutionRecord {
id: string;
sessionId: string;
query: string;
planText?: string;
planSteps?: Array<{ order: number; method: string; description: string }>;
generatedCode?: string;
partialCode?: string;
reportBlocks?: ReportBlock[];
retryCount: number;
status: AgentExecutionStatus;
errorMessage?: string;
durationMs?: number;
}
/** SSE 消息 */
export interface SSEMessage {