Files
memind/experience-schema.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

207 lines
6.8 KiB
JavaScript

export const EXPERIENCE_RESULTS = Object.freeze(['success', 'partial', 'failure', 'unknown']);
export const EXPERIENCE_STATUSES = Object.freeze(['active', 'archived', 'deleted']);
export const EXPERIENCE_KINDS = Object.freeze([
'lesson',
'task_outcome',
'execution_pattern',
'skill_candidate',
]);
export const EXPERIENCE_V1_MYSQL_DDL = Object.freeze([
'problem VARCHAR(512) NULL',
'environment_json JSON NULL',
'hypothesis VARCHAR(512) NULL',
'action_json JSON NULL',
"result ENUM('success','partial','failure','unknown') NULL",
'confidence DECIMAL(4,3) NULL',
'evidence_json JSON NULL',
'parent_experience_id CHAR(36) NULL',
'supersedes_id CHAR(36) NULL',
"status ENUM('active','archived','deleted') NOT NULL DEFAULT 'active'",
]);
function nonEmptyString(value, maxLen = null) {
const text = String(value ?? '').trim();
if (!text) return null;
return maxLen ? text.slice(0, maxLen) : text;
}
function normalizeConfidence(value) {
if (value == null || value === '') return null;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return null;
return Math.min(1, Math.max(0, parsed));
}
function normalizeResult(value) {
const text = String(value ?? '').trim().toLowerCase();
if (!text) return null;
return EXPERIENCE_RESULTS.includes(text) ? text : null;
}
function normalizeStatus(value) {
const text = String(value ?? 'active').trim().toLowerCase();
return EXPERIENCE_STATUSES.includes(text) ? text : 'active';
}
function normalizeKind(value) {
const text = String(value ?? 'lesson').trim().toLowerCase();
return EXPERIENCE_KINDS.includes(text) ? text : 'lesson';
}
export function serializeExperienceJson(value) {
if (value == null) return null;
if (typeof value === 'object') return JSON.stringify(value);
const trimmed = String(value).trim();
if (!trimmed) return null;
JSON.parse(trimmed);
return trimmed;
}
export function parseExperienceJson(value, fallback = null) {
if (value == null) return fallback;
if (typeof value === 'object') return value;
try {
return JSON.parse(String(value));
} catch {
return fallback;
}
}
/**
* Normalize caller input into stable DB-bound fields. Title/body remain required
* at the service layer; structured fields are optional.
*/
export function normalizeExperienceRecordInput(input = {}) {
return {
scope: nonEmptyString(input.scope, 64) ?? 'global',
kind: normalizeKind(input.kind),
title: nonEmptyString(input.title, 255),
body: nonEmptyString(input.body),
tags: Array.isArray(input.tags)
? [...new Set(input.tags.map((tag) => String(tag).trim()).filter(Boolean))]
: [],
sourceSessionId: nonEmptyString(input.sourceSessionId, 128),
sourceUserId: nonEmptyString(input.sourceUserId, 36),
problem: nonEmptyString(input.problem, 512),
environment: parseExperienceJson(input.environment ?? input.environmentJson, null),
hypothesis: nonEmptyString(input.hypothesis, 512),
action: parseExperienceJson(input.action ?? input.actionJson, null),
result: normalizeResult(input.result),
confidence: normalizeConfidence(input.confidence),
evidence: parseExperienceJson(input.evidence ?? input.evidenceJson, null),
parentExperienceId: nonEmptyString(input.parentExperienceId, 36),
supersedesId: nonEmptyString(input.supersedesId, 36),
status: normalizeStatus(input.status),
};
}
export function mapExperienceRow(row) {
return {
id: row.id,
scope: row.scope,
kind: row.kind,
title: row.title,
body: row.body,
tags: parseExperienceJson(row.tags_json ?? row.tags, []) ?? [],
sourceSessionId: row.source_session_id ?? null,
sourceUserId: row.source_user_id ?? null,
useCount: Number(row.use_count ?? 0),
createdAt: Number(row.created_at),
updatedAt: Number(row.updated_at),
problem: row.problem ?? null,
environment: parseExperienceJson(row.environment_json, null),
hypothesis: row.hypothesis ?? null,
action: parseExperienceJson(row.action_json, null),
result: row.result ?? null,
confidence: row.confidence == null ? null : Number(row.confidence),
evidence: parseExperienceJson(row.evidence_json, null),
parentExperienceId: row.parent_experience_id ?? null,
supersedesId: row.supersedes_id ?? null,
status: row.status ?? 'active',
};
}
export function experienceSearchHaystack(row) {
const environmentText = row.environment_json
? JSON.stringify(parseExperienceJson(row.environment_json, {}))
: '';
return [
row.title,
row.body,
row.problem,
environmentText,
]
.filter(Boolean)
.join('\n')
.toLowerCase();
}
export function formatExperienceInjectionBlock(hits = []) {
if (!Array.isArray(hits) || hits.length === 0) return '';
const lines = hits.map((hit) => formatExperienceInjectionLine(hit));
return `# 相关经验(供参考,来自历史任务)\n${lines.join('\n')}`;
}
export function formatExperienceInjectionLine(hit) {
const parts = [`- ${hit.title}: ${hit.body}`];
if (hit.problem) parts.push(` 问题: ${hit.problem}`);
if (hit.result) {
const conf = hit.confidence == null ? '' : `, conf=${hit.confidence}`;
parts.push(` 结果: ${hit.result}${conf}`);
}
const runtime = hit.environment?.runtime;
const components = Array.isArray(hit.environment?.components)
? hit.environment.components.filter(Boolean).join(' + ')
: '';
if (runtime || components) {
parts.push(` 环境: ${[runtime, components].filter(Boolean).join(' · ')}`);
}
return parts.join('\n');
}
/**
* Build a structured task_outcome payload for MindSpace agent jobs.
*/
export function buildMindspaceJobExperiencePayload({ claim, sessionId, parsed, jobId = null } = {}) {
const title = String(parsed?.title ?? claim?.instruction ?? '').trim().slice(0, 200);
const summary = String(parsed?.summary ?? '').trim();
if (!title || !summary) return null;
const runRef = jobId ? String(jobId) : null;
return normalizeExperienceRecordInput({
kind: 'task_outcome',
title,
body: summary,
problem: claim?.instruction ?? title,
result: parsed?.status === 'failed' ? 'failure' : 'success',
sourceSessionId: sessionId ?? null,
sourceUserId: claim?.userId ?? null,
environment: {
runtime: 'mindspace-agent-job',
components: ['goose', 'portal'],
},
action: {
executor: 'goose',
steps: ['agent_job_complete'],
artifacts: Array.isArray(parsed?.sourceAssetIds) ? parsed.sourceAssetIds : [],
},
evidence: {
sources: runRef
? [{
source_id: `job:${runRef}`,
source_type: 'agent_job',
actor: 'goose',
timestamp_ms: Date.now(),
modality: 'task_result',
}]
: [],
provenance: {
session_id: sessionId ?? null,
user_id: claim?.userId ?? null,
job_id: runRef,
},
},
});
}