b923e54eff
Extend h5_experience with structured fields, wire mindspace-agent-runner and agent-run-gateway to persist task_outcome records with provenance, and add local migration and verification scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
222 lines
6.8 KiB
JavaScript
222 lines
6.8 KiB
JavaScript
import { deriveUserFacingText } from './conversation-display.mjs';
|
|
import { normalizeExperienceRecordInput } from './experience-schema.mjs';
|
|
|
|
const RUN_METADATA_KEY = 'memindRun';
|
|
|
|
function safeJsonParse(value, fallback = null) {
|
|
if (value == null || value === '') return fallback;
|
|
if (typeof value === 'object') return value;
|
|
try {
|
|
return JSON.parse(String(value));
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function extractRunMessageText(userMessage) {
|
|
if (typeof userMessage?.content === 'string') return userMessage.content;
|
|
if (!Array.isArray(userMessage?.content)) return '';
|
|
return userMessage.content
|
|
.filter((item) => item?.type === 'text')
|
|
.map((item) => String(item.text ?? '').trim())
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
}
|
|
|
|
export function extractAgentRunProblem(row) {
|
|
const userMessage = safeJsonParse(row?.user_message_json, {}) ?? {};
|
|
const displayText = String(userMessage?.metadata?.displayText ?? '').trim();
|
|
const problem = displayText || deriveUserFacingText(extractRunMessageText(userMessage));
|
|
return problem.trim();
|
|
}
|
|
|
|
function resolveRunOptions(userMessage) {
|
|
const metadata = userMessage?.metadata ?? {};
|
|
const runMetadata = metadata?.[RUN_METADATA_KEY] ?? metadata?.agentRun ?? {};
|
|
return {
|
|
toolMode: String(runMetadata?.toolMode ?? metadata?.toolMode ?? 'chat').trim().toLowerCase(),
|
|
taskType: String(runMetadata?.taskType ?? metadata?.taskType ?? '').trim(),
|
|
requiredExecutor: String(runMetadata?.executor ?? runMetadata?.requiredExecutor ?? '').trim().toLowerCase(),
|
|
suggestedSkill: String(
|
|
runMetadata?.selectedChatSkill ?? metadata?.selectedChatSkill ?? '',
|
|
).trim(),
|
|
};
|
|
}
|
|
|
|
function summarizeToolEvidence(toolEvidence) {
|
|
if (!toolEvidence) return '';
|
|
const calls = Array.isArray(toolEvidence.calls)
|
|
? toolEvidence.calls.filter(Boolean)
|
|
: [];
|
|
if (calls.length > 0) {
|
|
return `工具调用: ${calls.join(', ')}`;
|
|
}
|
|
if (toolEvidence.generateImage?.succeeded === true) {
|
|
return '图片生成完成';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function summarizeRunEvents(events = []) {
|
|
for (const event of events) {
|
|
const data = safeJsonParse(event?.data_json ?? event?.dataJson, {}) ?? {};
|
|
if (event.event_type === 'tool_gateway_result' || event.eventType === 'tool_gateway_result') {
|
|
const stdout = String(data.stdoutTail ?? '').trim();
|
|
const stderr = String(data.stderrTail ?? '').trim();
|
|
const executor = String(data.executor ?? '').trim();
|
|
const parts = [];
|
|
if (executor) parts.push(`执行器 ${executor}`);
|
|
if (stdout) parts.push(stdout);
|
|
if (stderr) parts.push(`stderr: ${stderr}`);
|
|
if (parts.length > 0) return parts.join('\n').slice(0, 2000);
|
|
}
|
|
if (event.event_type === 'direct_chat_completed' || event.eventType === 'direct_chat_completed') {
|
|
const model = String(data.model ?? '').trim();
|
|
return model ? `Direct chat 完成 (${model})` : 'Direct chat 完成';
|
|
}
|
|
if (event.event_type === 'session_finished' || event.eventType === 'session_finished') {
|
|
const calls = Array.isArray(data.toolCalls) ? data.toolCalls.filter(Boolean) : [];
|
|
if (calls.length > 0) return `会话完成,工具: ${calls.join(', ')}`;
|
|
return 'Goose 会话完成';
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export function buildAgentRunExperienceBody({
|
|
status,
|
|
errorMessage = null,
|
|
toolEvidence = null,
|
|
eventSummary = null,
|
|
routing = null,
|
|
}) {
|
|
if (status === 'failed') {
|
|
const message = String(errorMessage ?? '').trim();
|
|
if (message) return message.slice(0, 4000);
|
|
return 'Agent run failed';
|
|
}
|
|
const parts = [
|
|
summarizeToolEvidence(toolEvidence),
|
|
String(eventSummary ?? '').trim(),
|
|
routing?.suggestedSkill
|
|
? `技能: ${routing.suggestedSkill}`
|
|
: routing?.route
|
|
? `路由: ${routing.route}`
|
|
: '',
|
|
'Agent run completed successfully.',
|
|
].filter(Boolean);
|
|
return parts.join('\n').slice(0, 4000);
|
|
}
|
|
|
|
export function buildAgentRunExperiencePayload({
|
|
row,
|
|
runId,
|
|
status,
|
|
sessionId = null,
|
|
errorMessage = null,
|
|
toolEvidence = null,
|
|
routing = null,
|
|
eventSummary = null,
|
|
} = {}) {
|
|
const terminalStatus = String(status ?? '').trim().toLowerCase();
|
|
if (!['succeeded', 'failed'].includes(terminalStatus)) return null;
|
|
|
|
const problem = extractAgentRunProblem(row);
|
|
const body = buildAgentRunExperienceBody({
|
|
status: terminalStatus,
|
|
errorMessage,
|
|
toolEvidence,
|
|
eventSummary,
|
|
routing,
|
|
});
|
|
if (!problem || !body) return null;
|
|
|
|
const userMessage = safeJsonParse(row?.user_message_json, {}) ?? {};
|
|
const runOptions = resolveRunOptions(userMessage);
|
|
const executor = runOptions.requiredExecutor
|
|
|| (routing?.route === 'direct_chat' ? 'direct-chat' : 'goose');
|
|
const components = [
|
|
routing?.route ?? null,
|
|
runOptions.toolMode !== 'chat' ? runOptions.toolMode : null,
|
|
runOptions.requiredExecutor || null,
|
|
runOptions.suggestedSkill || routing?.suggestedSkill || null,
|
|
'portal',
|
|
].filter(Boolean);
|
|
|
|
return normalizeExperienceRecordInput({
|
|
kind: 'task_outcome',
|
|
title: problem.slice(0, 200),
|
|
body,
|
|
problem,
|
|
result: terminalStatus === 'succeeded' ? 'success' : 'failure',
|
|
confidence: terminalStatus === 'succeeded' ? 0.7 : 0.6,
|
|
sourceSessionId: sessionId,
|
|
sourceUserId: row?.user_id ?? null,
|
|
environment: {
|
|
runtime: 'agent-run-gateway',
|
|
components,
|
|
},
|
|
action: {
|
|
executor,
|
|
steps: Array.isArray(toolEvidence?.calls) ? toolEvidence.calls : ['agent_run_complete'],
|
|
artifacts: [],
|
|
},
|
|
evidence: {
|
|
sources: [{
|
|
source_id: `run:${runId}`,
|
|
source_type: 'agent_run',
|
|
actor: executor,
|
|
timestamp_ms: Date.now(),
|
|
modality: terminalStatus === 'failed' ? 'run_failure' : 'run_result',
|
|
}],
|
|
provenance: {
|
|
run_id: runId,
|
|
session_id: sessionId,
|
|
user_id: row?.user_id ?? null,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function loadAgentRunEventSummary(pool, runId) {
|
|
if (!pool?.query || !runId) return '';
|
|
const [rows] = await pool.query(
|
|
`SELECT event_type, data_json
|
|
FROM h5_agent_run_events
|
|
WHERE run_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT 20`,
|
|
[runId],
|
|
);
|
|
return summarizeRunEvents(Array.isArray(rows) ? rows : []);
|
|
}
|
|
|
|
export async function extractAgentRunExperience({
|
|
experienceService,
|
|
pool = null,
|
|
row,
|
|
runId,
|
|
status,
|
|
sessionId = null,
|
|
errorMessage = null,
|
|
toolEvidence = null,
|
|
routing = null,
|
|
}) {
|
|
if (!experienceService || typeof experienceService.record !== 'function') {
|
|
return null;
|
|
}
|
|
const eventSummary = pool ? await loadAgentRunEventSummary(pool, runId).catch(() => '') : '';
|
|
const payload = buildAgentRunExperiencePayload({
|
|
row,
|
|
runId,
|
|
status,
|
|
sessionId,
|
|
errorMessage,
|
|
toolEvidence,
|
|
routing,
|
|
eventSummary,
|
|
});
|
|
if (!payload) return null;
|
|
return experienceService.record(payload);
|
|
}
|