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:
2026-03-11 22:49:05 +08:00
parent d3b24bd8c3
commit 6edfad032f
19 changed files with 2105 additions and 158 deletions

View File

@@ -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: '❌ 已取消当前分析',
};