feat: add task artifact validation for code runs

This commit is contained in:
Your Name
2026-07-02 12:31:29 +08:00
parent 201cc5fb7a
commit 7a47dc6a98
5 changed files with 251 additions and 14 deletions
+8 -2
View File
@@ -13,6 +13,7 @@ import {
uploadMindSpaceAsset,
subscribeSessionEvents,
} from '../api/client';
import type { AgentRun } from '../api/client';
import type { ChatState, Message, MindSpaceChatContext, PortalUser, Session, SessionEvent } from '../types';
import { buildContextPrefix } from '../utils/mindspaceChatContext';
import { buildUserAddressPrefix } from '../utils/userAddress';
@@ -29,8 +30,8 @@ import {
pushMessage,
} from '../utils/message';
async function waitForAgentRun(runId: string) {
return await new Promise((resolve, reject) => {
async function waitForAgentRun(runId: string): Promise<AgentRun> {
return await new Promise<AgentRun>((resolve, reject) => {
const unsubscribe = subscribeAgentRunEvents(
runId,
(run) => {
@@ -349,6 +350,11 @@ export function usePageEditSubChat({
forceCode: true,
userId: user?.id ?? null,
requestId,
mindspaceContext: context,
pageEdit: {
pageId,
pageTitle,
},
}),
);
const finishedRun =
+4 -2
View File
@@ -24,6 +24,7 @@ import {
syncUserMemory,
updateProvider,
} from '../api/client';
import type { AgentRun } from '../api/client';
import { appConfig } from '../config';
import {
clearStoredSessionId,
@@ -74,8 +75,8 @@ const FINISH_SYNC_RETRY_DELAYS_MS = [500, 1500, 3000];
const ACTIVE_REQUEST_MISSING_GRACE_MS = 2500;
export { INSUFFICIENT_BALANCE_NOTICE };
async function waitForAgentRun(runId: string) {
return await new Promise((resolve, reject) => {
async function waitForAgentRun(runId: string): Promise<AgentRun> {
return await new Promise<AgentRun>((resolve, reject) => {
const unsubscribe = subscribeAgentRunEvents(
runId,
(run) => {
@@ -1177,6 +1178,7 @@ export function useTKMindChat(
taskType: 'h5_chat_code_task',
userId: userRef.current?.id ?? null,
requestId,
mindspaceContext: options?.mindspaceContext ?? null,
}),
);
const finishedRun =
+139 -3
View File
@@ -1,3 +1,5 @@
import type { MindSpaceChatContext } from '../types';
export type AgentRunCreateOptions = {
toolMode?: 'chat' | 'code';
taskType?: string | null;
@@ -55,6 +57,128 @@ function sanitizeRequestIdForPath(requestId: string): string {
return normalized || 'unknown-request';
}
function normalizeExpectedFile(value: AgentRunValidationFile | string): AgentRunValidationFile | null {
if (typeof value === 'string') {
const path = value.trim();
return path ? { path } : null;
}
const path = String(value?.path ?? '').trim();
if (!path) return null;
return {
path,
...(value.contains == null ? {} : { contains: String(value.contains) }),
};
}
function mergeAgentRunValidationFiles(
...items: Array<AgentRunValidation | AgentRunValidationFile | string | null | undefined>
): AgentRunValidation | null {
const byKey = new Map<string, AgentRunValidationFile>();
const addFile = (file: AgentRunValidationFile | string | null | undefined) => {
if (!file) return;
const normalized = normalizeExpectedFile(file);
if (!normalized) return;
const key = `${normalized.path}\u0000${normalized.contains ?? ''}`;
byKey.set(key, normalized);
};
for (const item of items) {
if (!item) continue;
if (typeof item === 'string' || 'path' in item) {
addFile(item);
continue;
}
addFile(item.expectedFile);
for (const file of item.expectedFiles ?? []) addFile(file);
}
const expectedFiles = [...byKey.values()];
return expectedFiles.length ? { expectedFiles } : null;
}
export function normalizeAgentRunPublicHtmlPath(candidate: string): string | null {
const decoded = decodeURIComponent(String(candidate ?? '')).replace(/\\/g, '/').trim();
const publicMatch = decoded.match(/(?:^|\/)(public\/[^"'<>?#\s]+\.html)\b/i);
const raw = publicMatch?.[1] ?? '';
if (!raw) return null;
const parts = raw
.split('/')
.map((part) => part.trim())
.filter(Boolean);
if (parts.length < 2 || parts[0] !== 'public') return null;
if (parts.some((part) => part === '.' || part === '..')) return null;
return parts.join('/');
}
export function extractAgentRunPublicHtmlPaths(...values: Array<string | null | undefined>): string[] {
const found = new Set<string>();
const pattern = /(?:^|[\s"'([{<])((?:https?:\/\/[^\s"'<>]+|\/?MindSpace\/[^\s"'<>]+|public\/[^\s"'<>]+)\.html)\b/gi;
for (const value of values) {
const text = String(value ?? '');
let match: RegExpExecArray | null;
while ((match = pattern.exec(text))) {
const path = normalizeAgentRunPublicHtmlPath(match[1] ?? '');
if (path) found.add(path);
}
}
return [...found];
}
export function buildAgentRunTaskValidation({
requestId,
taskType,
text,
mindspaceContext,
pageEdit,
}: {
requestId: string;
taskType: string;
text: string;
mindspaceContext?: MindSpaceChatContext | null;
pageEdit?: { pageId?: string | null; pageTitle?: string | null } | null;
}): { validation: AgentRunValidation | null; instruction: string } {
const safeRequestId = sanitizeRequestIdForPath(requestId);
const pageId = String(pageEdit?.pageId ?? mindspaceContext?.page?.id ?? '').trim();
const pageTitle = String(pageEdit?.pageTitle ?? mindspaceContext?.page?.title ?? '').trim();
const publicHtmlPaths = extractAgentRunPublicHtmlPaths(
text,
mindspaceContext?.page?.publicationUrl ?? null,
);
const expectedFiles: AgentRunValidationFile[] = publicHtmlPaths.map((path) => ({ path }));
const instructions: string[] = [];
if (publicHtmlPaths.length > 0) {
instructions.push(
'',
'[Memind task artifact validation]',
'The task references concrete MindSpace public HTML artifacts.',
'Before finishing, ensure these workspace-relative files exist and contain the intended result:',
...publicHtmlPaths.map((path) => `- ${path}`),
);
}
if (taskType === 'page_edit_code_task' && pageId) {
const taskReceiptPath = `.memind/agent-runs/${safeRequestId}-page-edit.json`;
expectedFiles.push({ path: taskReceiptPath, contains: pageId });
instructions.push(
'',
'[Memind page-edit validation]',
`Before finishing this page-edit task, create or update ${taskReceiptPath}.`,
'The file must be valid JSON and include:',
`- requestId: ${requestId}`,
`- taskType: ${taskType}`,
`- pageId: ${pageId}`,
...(pageTitle ? [`- pageTitle: ${pageTitle}`] : []),
'- a brief summary of the page changes or the reason no change was needed.',
);
}
return {
validation: expectedFiles.length ? { expectedFiles } : null,
instruction: instructions.join('\n'),
};
}
export function buildAgentRunValidationReceipt(requestId: string): {
validation: AgentRunValidation;
instruction: string;
@@ -88,23 +212,35 @@ export function resolveAgentRunOptions(
allowAutodetect = agentCodeRunsAutodetectEnabled,
userId = null,
requestId = null,
mindspaceContext = null,
pageEdit = null,
}: {
taskType?: string;
forceCode?: boolean;
allowAutodetect?: boolean;
userId?: string | null;
requestId?: string | null;
mindspaceContext?: MindSpaceChatContext | null;
pageEdit?: { pageId?: string | null; pageTitle?: string | null } | null;
} = {},
): AgentRunCreateOptions {
if (!agentCodeRunsEnabledForUser(userId)) return {};
const normalizedText = String(text ?? '').trim();
const shouldUseCode = forceCode || (allowAutodetect && CODE_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText)));
if (!shouldUseCode) return {};
const receipt = buildAgentRunValidationReceipt(requestId ?? crypto.randomUUID());
const normalizedRequestId = requestId ?? crypto.randomUUID();
const receipt = buildAgentRunValidationReceipt(normalizedRequestId);
const taskValidation = buildAgentRunTaskValidation({
requestId: normalizedRequestId,
taskType,
text: normalizedText,
mindspaceContext,
pageEdit,
});
return {
toolMode: 'code',
taskType,
validation: receipt.validation,
validationInstruction: receipt.instruction,
validation: mergeAgentRunValidationFiles(receipt.validation, taskValidation.validation),
validationInstruction: `${receipt.instruction}${taskValidation.instruction}`,
};
}