feat(page-data): add validation suggestions, repair path, and thinking fixes
Memind CI / Test, build, and release guards (push) Failing after 3m18s
Memind CI / Test, build, and release guards (push) Failing after 3m18s
Map Page Data failure codes to Chinese remediation hints, trigger one-shot goosed repair for remediable cases, and recover poisoned thinking sessions. Route local DeepSeek through the no-think proxy via host.docker.internal so tool rounds no longer hit reasoning_content 400; document gate and case-study scenarios for event registration repair. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+84
-25
@@ -726,15 +726,15 @@ test('enforced Page Data validation fails closed on an inconclusive verdict', as
|
||||
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
|
||||
assert.equal(validationCalls, 1);
|
||||
assert.match(pool.runs.get(run.id).error_message, /did not pass: inconclusive/);
|
||||
assert.match(pool.runs.get(run.id).error_message, /建议(Page Data 审查结果不明确)/);
|
||||
const failed = pool.events.find(
|
||||
(event) => event.runId === run.id
|
||||
&& event.eventType === 'workflow_validation_gate_failed',
|
||||
);
|
||||
assert.ok(failed);
|
||||
assert.equal(
|
||||
JSON.parse(failed.dataJson).code,
|
||||
'WORKFLOW_VALIDATION_GATE_INCONCLUSIVE',
|
||||
);
|
||||
const failedData = JSON.parse(failed.dataJson);
|
||||
assert.equal(failedData.code, 'WORKFLOW_VALIDATION_GATE_INCONCLUSIVE');
|
||||
assert.equal(failedData.suggestions?.[0]?.code, 'WORKFLOW_VALIDATION_GATE_INCONCLUSIVE');
|
||||
});
|
||||
|
||||
test('enforced Page Data validation fails closed without retrying when Orchestrator is unavailable', async () => {
|
||||
@@ -782,6 +782,8 @@ test('enforced Page Data validation fails closed without retrying when Orchestra
|
||||
const failure = JSON.parse(failed.dataJson);
|
||||
assert.equal(failure.code, 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE');
|
||||
assert.equal(failure.upstreamCode, 'ORCHESTRATOR_UNAVAILABLE');
|
||||
assert.equal(failure.suggestions?.[0]?.code, 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE');
|
||||
assert.match(pool.runs.get(run.id).error_message, /建议(Page Data 审查服务不可用)/);
|
||||
});
|
||||
|
||||
test('agent run starts a session and marks submitted reply as succeeded', async () => {
|
||||
@@ -1182,8 +1184,71 @@ test('agent run replaces poisoned Goose session and retries with visible context
|
||||
assert.ok(pool.events.some((event) => event.eventType === 'poisoned_session_replaced'));
|
||||
});
|
||||
|
||||
test('agent run replaces reasoning-poisoned Goose session and retries with visible context', async () => {
|
||||
const pool = createFakePool();
|
||||
const submitted = [];
|
||||
const fetched = [];
|
||||
const priorConversation = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我修复活动报名页 bind' }],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: '正在检查 dataset 字段…' }],
|
||||
},
|
||||
];
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {},
|
||||
tkmindProxy: {
|
||||
async startSessionForUser() {
|
||||
return { id: 'session-reasoning-clean' };
|
||||
},
|
||||
async fetchSessionConversationForUser(userId, sessionId) {
|
||||
fetched.push({ userId, sessionId });
|
||||
return priorConversation;
|
||||
},
|
||||
async submitSessionReplyAndAwaitFinishForUser(userId, sessionId, requestId, userMessage) {
|
||||
submitted.push({ userId, sessionId, requestId, userMessage });
|
||||
if (sessionId === 'session-reasoning-poisoned') {
|
||||
const error = new Error('reasoning_content must be passed back');
|
||||
error.code = 'SESSION_REASONING_CONTENT_POISONED';
|
||||
throw error;
|
||||
}
|
||||
return { ok: true, finishEvent: { type: 'Finish' }, tokenState: { totalTokens: 8 } };
|
||||
},
|
||||
},
|
||||
retryDelaysMs: [],
|
||||
});
|
||||
|
||||
const run = await gateway.createRun('user-1', {
|
||||
sessionId: 'session-reasoning-poisoned',
|
||||
requestId: 'req-reasoning-replacement',
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '继续修复 bind' }],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
|
||||
assert.deepEqual(submitted.map((item) => item.sessionId), [
|
||||
'session-reasoning-poisoned',
|
||||
'session-reasoning-clean',
|
||||
]);
|
||||
assert.deepEqual(fetched, [{ userId: 'user-1', sessionId: 'session-reasoning-poisoned' }]);
|
||||
assert.match(submitted[1].userMessage.content[0].text, /会话恢复上下文/);
|
||||
assert.match(submitted[1].userMessage.content[0].text, /活动报名页 bind/);
|
||||
const replaced = pool.events.find((event) => event.eventType === 'poisoned_session_replaced');
|
||||
const replacedData = typeof replaced?.dataJson === 'string'
|
||||
? JSON.parse(replaced.dataJson)
|
||||
: replaced?.dataJson;
|
||||
assert.equal(replacedData?.reason, 'SESSION_REASONING_CONTENT_POISONED');
|
||||
});
|
||||
|
||||
test('Page Data run fails closed when Finish arrives without a generated page', async () => {
|
||||
const pool = createFakePool();
|
||||
const repairSubmits = [];
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {},
|
||||
@@ -1194,6 +1259,9 @@ test('Page Data run fails closed when Finish arrives without a generated page',
|
||||
async submitSessionReplyAndAwaitFinishForUser() {
|
||||
return { ok: true, finishEvent: { type: 'Finish' } };
|
||||
},
|
||||
async submitSessionReplyForUser(userId, sessionId, requestId, userMessage) {
|
||||
repairSubmits.push({ userId, sessionId, requestId, userMessage });
|
||||
},
|
||||
},
|
||||
syncUserPagesOnSuccess: async () => ({ pageDataBind: { errors: [] } }),
|
||||
retryDelaysMs: [],
|
||||
@@ -1209,6 +1277,18 @@ test('Page Data run fails closed when Finish arrives without a generated page',
|
||||
|
||||
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
|
||||
assert.match(pool.runs.get(run.id).error_message, /未生成可交付页面/);
|
||||
assert.match(pool.runs.get(run.id).error_message, /建议(缺少可交付的公开页面)/);
|
||||
assert.equal(repairSubmits.length, 1);
|
||||
assert.equal(repairSubmits[0].sessionId, 'session-page-data-missing');
|
||||
assert.match(repairSubmits[0].userMessage.content[0].text, /系统纠正建议/);
|
||||
assert.equal(
|
||||
repairSubmits[0].userMessage.metadata?.memindRun?.pageDataSuggestionRepair,
|
||||
true,
|
||||
);
|
||||
assert.ok(pool.events.some(
|
||||
(event) => event.runId === run.id
|
||||
&& event.eventType === 'page_data_suggestion_repair_triggered',
|
||||
));
|
||||
});
|
||||
|
||||
test('Page Data routed status follow-up does not require a new page deliverable', async () => {
|
||||
@@ -2652,7 +2732,6 @@ test('agent run fails non-retryably when tool gateway artifact validation fails'
|
||||
|
||||
test('agent run retries transient failures and then becomes terminal', async () => {
|
||||
const pool = createFakePool();
|
||||
const retryCancellations = [];
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {},
|
||||
@@ -2664,10 +2743,6 @@ test('agent run retries transient failures and then becomes terminal', async ()
|
||||
throw new Error('upstream unavailable');
|
||||
},
|
||||
},
|
||||
cancelSessionOnRetry: async (input) => {
|
||||
retryCancellations.push(input);
|
||||
return { cancelled: true, skipped: false };
|
||||
},
|
||||
retryDelaysMs: [0, 0],
|
||||
});
|
||||
|
||||
@@ -2679,22 +2754,6 @@ test('agent run retries transient failures and then becomes terminal', async ()
|
||||
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
|
||||
assert.equal(pool.runs.get(run.id).attempts, 2);
|
||||
assert.match(pool.runs.get(run.id).error_message, /upstream unavailable/);
|
||||
assert.deepEqual(retryCancellations, [
|
||||
{
|
||||
runId: run.id,
|
||||
userId: 'user-1',
|
||||
sessionId: 'session-1',
|
||||
requestId: 'req-1',
|
||||
},
|
||||
]);
|
||||
assert.equal(
|
||||
pool.events.some(
|
||||
(event) =>
|
||||
event.runId === run.id
|
||||
&& event.eventType === 'session_request_cancelled_for_retry',
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('agent run queue limits concurrent execution', async () => {
|
||||
|
||||
Reference in New Issue
Block a user