feat(ssa): finalize strict stepwise agent execution flow
Align Agent mode to strict stepwise generation and execution, add deterministic and safety hardening, and sync deployment/module documentation for Phase 5A.5/5B/5C rollout. - implement strict stepwise execution path and dependency short-circuiting - persist step-level errors/results and stream step_* progress events - add agent plan params patch route and schema/migration support - improve R sanitizer/security checks and step result rendering in workspace - update SSA module guide and deployment change checklist Made-with: Cursor
This commit is contained in:
@@ -17,7 +17,7 @@ import { useState, useCallback, useRef } from 'react';
|
||||
import { getAccessToken, isTokenExpired, refreshAccessToken } from '@/framework/auth/api';
|
||||
import { useSSAStore } from '../stores/ssaStore';
|
||||
import type { AskUserEventData, AskUserResponseData } from '../components/AskUserCard';
|
||||
import type { WorkflowPlan } from '../types';
|
||||
import type { WorkflowPlan, AgentStepResult } from '../types';
|
||||
|
||||
// ────────────────────────────────────────────
|
||||
// Types
|
||||
@@ -229,7 +229,19 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
setIntentMeta(null);
|
||||
setPendingQuestion(null);
|
||||
|
||||
const isAgentAction = !!metadata?.agentAction;
|
||||
const { executionMode, agentExecution } = useSSAStore.getState();
|
||||
const isAgentInlineInstruction =
|
||||
executionMode === 'agent'
|
||||
&& !!agentExecution
|
||||
&& (agentExecution.status === 'plan_pending' || agentExecution.status === 'code_pending')
|
||||
&& !metadata?.agentAction
|
||||
&& !metadata?.askUserResponse;
|
||||
|
||||
const finalMetadata = isAgentInlineInstruction
|
||||
? { ...(metadata || {}), agentInlineInstruction: true }
|
||||
: metadata;
|
||||
|
||||
const isAgentAction = !!finalMetadata?.agentAction || isAgentInlineInstruction;
|
||||
|
||||
const assistantMsgId = crypto.randomUUID();
|
||||
const assistantPlaceholder: ChatMessage = {
|
||||
@@ -254,7 +266,8 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
setChatMessages(prev => [...prev, userMsg, assistantPlaceholder]);
|
||||
}
|
||||
|
||||
abortRef.current = new AbortController();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
let fullContent = '';
|
||||
let fullThinking = '';
|
||||
|
||||
@@ -267,8 +280,8 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ content, ...(metadata ? { metadata } : {}) }),
|
||||
signal: abortRef.current.signal,
|
||||
body: JSON.stringify({ content, ...(finalMetadata ? { metadata: finalMetadata } : {}) }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -359,10 +372,115 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
|
||||
if (parsed.type === 'agent_plan_ready') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
const initialStepResults: AgentStepResult[] = Array.isArray(parsed.plan?.steps)
|
||||
? parsed.plan.steps.map((s: any) => ({
|
||||
stepOrder: s.order || 0,
|
||||
method: s.method || '',
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
}))
|
||||
: [];
|
||||
updateAgentExecution({
|
||||
planText: parsed.planText,
|
||||
planSteps: parsed.plan?.steps,
|
||||
status: 'plan_pending',
|
||||
stepResults: initialStepResults.length > 0 ? initialStepResults : undefined,
|
||||
currentStep: initialStepResults.length > 0 ? initialStepResults[0].stepOrder : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const patchStepResult = (stepOrder: number, patch: Partial<AgentStepResult>) => {
|
||||
const { agentExecution, updateAgentExecution } = useSSAStore.getState();
|
||||
const existing = agentExecution?.stepResults || [];
|
||||
const idx = existing.findIndex(s => s.stepOrder === stepOrder);
|
||||
let next: AgentStepResult[];
|
||||
if (idx >= 0) {
|
||||
next = existing.map((s, i) => (i === idx ? { ...s, ...patch } : s));
|
||||
} else {
|
||||
next = [
|
||||
...existing,
|
||||
{
|
||||
stepOrder,
|
||||
method: '',
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
...patch,
|
||||
} as AgentStepResult,
|
||||
].sort((a, b) => a.stepOrder - b.stepOrder);
|
||||
}
|
||||
updateAgentExecution({ stepResults: next, currentStep: stepOrder });
|
||||
};
|
||||
|
||||
if (parsed.type === 'step_coding') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'coding',
|
||||
partialCode: parsed.partialCode,
|
||||
retryCount: parsed.retryCount || 0,
|
||||
});
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
partialCode: parsed.partialCode,
|
||||
status: 'coding',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_code_ready') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'coding',
|
||||
code: parsed.code,
|
||||
partialCode: undefined,
|
||||
});
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
generatedCode: parsed.code,
|
||||
partialCode: undefined,
|
||||
status: 'coding',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_executing') {
|
||||
patchStepResult(parsed.stepOrder || 0, { status: 'executing' });
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({ status: 'executing' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_result') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'completed',
|
||||
reportBlocks: parsed.reportBlocks,
|
||||
durationMs: parsed.durationMs,
|
||||
errorMessage: undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_error') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: parsed.willRetry ? 'coding' : 'error',
|
||||
errorMessage: parsed.message,
|
||||
retryCount: parsed.retryCount || 0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_skipped') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'skipped',
|
||||
errorMessage: parsed.reason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'pipeline_aborted') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
status: 'error',
|
||||
errorMessage: parsed.error || '执行已终止',
|
||||
currentStep: parsed.stepOrder,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -379,9 +497,9 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
if (parsed.type === 'code_generated') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
generatedCode: parsed.code,
|
||||
generatedCode: parsed.code || undefined,
|
||||
partialCode: undefined,
|
||||
status: parsed.code ? 'code_pending' : 'coding',
|
||||
status: 'code_pending',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -399,6 +517,7 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
generatedCode: parsed.code || curExec?.generatedCode,
|
||||
status: 'completed',
|
||||
durationMs: parsed.durationMs,
|
||||
stepResults: parsed.stepResults || curExec?.stepResults,
|
||||
});
|
||||
|
||||
// 在对话中插入可点击的结果卡片
|
||||
@@ -503,7 +622,9 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
: m
|
||||
));
|
||||
setIsGenerating(false);
|
||||
abortRef.current = null;
|
||||
if (abortRef.current === controller) {
|
||||
abortRef.current = null;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
return sendChatMessage(sessionId, content, metadata);
|
||||
}
|
||||
@@ -521,7 +642,9 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
setIsGenerating(false);
|
||||
setStreamingContent('');
|
||||
setThinkingContent('');
|
||||
abortRef.current = null;
|
||||
if (abortRef.current === controller) {
|
||||
abortRef.current = null;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -533,7 +656,7 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
*/
|
||||
const executeAgentAction = useCallback(async (sessionId: string, action: AgentActionType) => {
|
||||
const AUDIT_MESSAGES: Record<AgentActionType, string> = {
|
||||
confirm_plan: '✅ 方案已确认,正在生成 R 代码...',
|
||||
confirm_plan: '✅ 方案已确认,已进入执行确认(执行时将分步生成代码)...',
|
||||
confirm_code: '✅ 代码已确认,R 引擎正在执行...',
|
||||
cancel: '❌ 已取消当前分析',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user