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
+12 -13
View File
@@ -6,6 +6,8 @@ import { jsonrepair } from 'jsonrepair';
import { reconcileAgentSession } from './session-reconcile.mjs';
import { resolveSessionAccess } from './session-broker.mjs';
import { resolveBillingTokenState } from './billing-token-state.mjs';
import { buildMindspaceJobExperiencePayload } from './experience-schema.mjs';
import { formatExperienceInjectionBlock } from './experience-service.mjs';
const insecureDispatcher = new Agent({
connect: { rejectUnauthorized: false },
@@ -429,7 +431,7 @@ export function createMindSpaceAgentRunner({
sourceAssetIds: claim.allowedAssets.map((asset) => asset.assetId),
});
// Record the outcome as shared experience so other instances benefit.
await recordExperience(claim, sessionId, parsed);
await recordExperience(jobId, claim, sessionId, parsed);
return completed;
} catch (error) {
if (claim?.jobToken) {
@@ -452,27 +454,24 @@ export function createMindSpaceAgentRunner({
try {
const hits = await experienceService.search(instruction, { limit: 3 });
if (!hits.length) return '';
const lines = hits.map((h) => `- ${h.title}: ${h.body}`).join('\n');
return `# 相关经验(供参考,来自历史任务)\n${lines}`;
return formatExperienceInjectionBlock(hits);
} catch {
return '';
}
}
// Persists a one-line lesson from a completed job. Best-effort; never throws.
async function recordExperience(claim, sessionId, parsed) {
async function recordExperience(jobId, claim, sessionId, parsed) {
if (!experienceService) return;
try {
const title = String(parsed?.title ?? claim.instruction ?? '').slice(0, 200);
const summary = String(parsed?.summary ?? '').trim();
if (!title || !summary) return;
await experienceService.record({
kind: 'task_outcome',
title,
body: summary,
sourceSessionId: sessionId,
sourceUserId: claim.userId,
const payload = buildMindspaceJobExperiencePayload({
claim,
sessionId,
parsed,
jobId,
});
if (!payload) return;
await experienceService.record(payload);
} catch {
// swallow — experience recording is non-critical
}