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