Files
memind/experience-extractor.mjs
T
john b923e54eff Add Experience V1 schema migration and agent-run completion extractor.
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>
2026-09-02 10:26:46 +08:00

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);
}