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:
john
2026-09-02 10:26:46 +08:00
parent 908db04b67
commit b923e54eff
19 changed files with 1340 additions and 131 deletions
+110
View File
@@ -0,0 +1,110 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildMindspaceJobExperiencePayload,
formatExperienceInjectionBlock,
formatExperienceInjectionLine,
mapExperienceRow,
normalizeExperienceRecordInput,
serializeExperienceJson,
} from './experience-schema.mjs';
test('normalizeExperienceRecordInput dedupes tags and defaults kind/status', () => {
const normalized = normalizeExperienceRecordInput({
title: ' 标题 ',
body: '正文',
tags: ['a', 'a', ' b ', ''],
kind: 'TASK_OUTCOME',
status: 'ACTIVE',
});
assert.equal(normalized.kind, 'task_outcome');
assert.equal(normalized.status, 'active');
assert.deepEqual(normalized.tags, ['a', 'b']);
});
test('normalizeExperienceRecordInput clamps confidence and validates result', () => {
const normalized = normalizeExperienceRecordInput({
title: 't',
body: 'b',
confidence: 1.5,
result: 'SUCCESS',
});
assert.equal(normalized.confidence, 1);
assert.equal(normalized.result, 'success');
});
test('serializeExperienceJson accepts objects and valid JSON strings', () => {
assert.equal(serializeExperienceJson({ a: 1 }), '{"a":1}');
assert.equal(serializeExperienceJson('{"a":1}'), '{"a":1}');
assert.equal(serializeExperienceJson(''), null);
});
test('mapExperienceRow parses JSON columns', () => {
const mapped = mapExperienceRow({
id: 'id-1',
scope: 'global',
kind: 'lesson',
title: 't',
body: 'b',
tags_json: '["x"]',
use_count: 2,
environment_json: '{"runtime":"portal"}',
evidence_json: '{"sources":[]}',
status: 'active',
created_at: 1,
updated_at: 2,
});
assert.deepEqual(mapped.tags, ['x']);
assert.deepEqual(mapped.environment, { runtime: 'portal' });
assert.equal(mapped.useCount, 2);
});
test('formatExperienceInjectionLine includes structured hints', () => {
const line = formatExperienceInjectionLine({
title: '部署',
body: '摘要',
problem: '公网访问失败',
result: 'success',
confidence: 0.9,
environment: { runtime: '103', components: ['caddy', 'portal'] },
});
assert.match(line, /部署: 摘要/);
assert.match(line, /问题: 公网访问失败/);
assert.match(line, /结果: success, conf=0.9/);
assert.match(line, /环境: 103 · caddy \+ portal/);
});
test('formatExperienceInjectionBlock wraps lines with header', () => {
const block = formatExperienceInjectionBlock([
{ title: 'A', body: 'one' },
{ title: 'B', body: 'two' },
]);
assert.match(block, /^# 相关经验/);
assert.match(block, /- A: one/);
assert.match(block, /- B: two/);
});
test('buildMindspaceJobExperiencePayload maps failed jobs to failure result', () => {
const payload = buildMindspaceJobExperiencePayload({
claim: { instruction: '生成页面', userId: 'u1' },
sessionId: 's1',
parsed: { title: '页面标题', summary: '完成摘要', status: 'failed' },
jobId: 'job-42',
});
assert.equal(payload.kind, 'task_outcome');
assert.equal(payload.result, 'failure');
assert.equal(payload.problem, '生成页面');
assert.equal(payload.sourceSessionId, 's1');
assert.equal(payload.evidence.provenance.job_id, 'job-42');
});
test('buildMindspaceJobExperiencePayload returns null when title or summary missing', () => {
assert.equal(
buildMindspaceJobExperiencePayload({
claim: { instruction: 'x' },
sessionId: 's',
parsed: { title: '', summary: 'ok' },
}),
null,
);
});