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>
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user