feat: add fail-closed Aider development skill
This commit is contained in:
@@ -327,6 +327,26 @@ function normalizeSessionMessageCount(value) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
export function resolveRequiredCodeExecutor(userMessage) {
|
||||
const metadata = userMessage?.metadata;
|
||||
const runMetadata = metadata?.[RUN_METADATA_KEY] ?? metadata?.agentRun ?? {};
|
||||
const executor = String(runMetadata?.executor ?? '').trim().toLowerCase();
|
||||
return ['aider', 'openhands'].includes(executor) ? executor : null;
|
||||
}
|
||||
|
||||
export function assertRequiredCodeExecutorAvailable(requiredExecutor, toolGatewayStatus) {
|
||||
if (!requiredExecutor) return;
|
||||
const executors = Array.isArray(toolGatewayStatus?.executors)
|
||||
? toolGatewayStatus.executors.map((item) => String(item).trim().toLowerCase())
|
||||
: [];
|
||||
if (!toolGatewayStatus?.enabled || !executors.includes(requiredExecutor)) {
|
||||
const error = new Error(`必需执行器 ${requiredExecutor} 不可用,任务已停止且不会回退`);
|
||||
error.code = 'REQUIRED_EXECUTOR_UNAVAILABLE';
|
||||
error.retryable = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getRunOptionsFromMessage(userMessage) {
|
||||
const metadata = userMessage?.metadata;
|
||||
const runMetadata = metadata?.[RUN_METADATA_KEY] ?? metadata?.agentRun ?? {};
|
||||
@@ -339,6 +359,7 @@ function getRunOptionsFromMessage(userMessage) {
|
||||
return {
|
||||
toolMode,
|
||||
taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType),
|
||||
requiredExecutor: resolveRequiredCodeExecutor(userMessage),
|
||||
forceDeepReasoning: runMetadata?.forceDeepReasoning === true || metadata?.forceDeepReasoning === true,
|
||||
validation: normalizeToolGatewayValidation(runMetadata?.validation ?? metadata?.toolGatewayValidation),
|
||||
sessionMessageCount: normalizeSessionMessageCount(
|
||||
@@ -700,6 +721,7 @@ export function createAgentRunGateway({
|
||||
let userMessage = safeJsonParse(row.user_message_json, {});
|
||||
const runOptions = getRunOptionsFromMessage(userMessage);
|
||||
const toolGatewayStatus = toolGateway?.getStatus ? toolGateway.getStatus() : null;
|
||||
assertRequiredCodeExecutorAvailable(runOptions.requiredExecutor, toolGatewayStatus);
|
||||
let disclosureDecision = null;
|
||||
try {
|
||||
disclosureDecision = systemDisclosurePolicyService?.evaluate?.({
|
||||
@@ -878,6 +900,17 @@ export function createAgentRunGateway({
|
||||
cwd: workingDir,
|
||||
timeoutMs: runTimeoutMs,
|
||||
});
|
||||
if (
|
||||
runOptions.requiredExecutor &&
|
||||
String(result?.executor ?? '').trim().toLowerCase() !== runOptions.requiredExecutor
|
||||
) {
|
||||
const error = new Error(
|
||||
`执行器不匹配:要求 ${runOptions.requiredExecutor},实际 ${result?.executor ?? 'unknown'}`,
|
||||
);
|
||||
error.code = 'REQUIRED_EXECUTOR_MISMATCH';
|
||||
error.retryable = false;
|
||||
throw error;
|
||||
}
|
||||
await appendEvent(runId, 'tool_gateway_result', {
|
||||
executor: result.executor ?? null,
|
||||
dryRun: Boolean(result.dryRun),
|
||||
|
||||
@@ -5,10 +5,42 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
assertRequiredCodeExecutorAvailable,
|
||||
assertRequiredImageGenerationCompleted,
|
||||
createAgentRunGateway,
|
||||
resolveRequiredCodeExecutor,
|
||||
} from './agent-run-gateway.mjs';
|
||||
|
||||
test('required code executor is read from run metadata', () => {
|
||||
assert.equal(resolveRequiredCodeExecutor({
|
||||
metadata: { memindRun: { executor: 'AIDER' } },
|
||||
}), 'aider');
|
||||
assert.equal(resolveRequiredCodeExecutor({
|
||||
metadata: { memindRun: { executor: 'unknown' } },
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('required Aider executor fails closed when Tool Gateway is unavailable', () => {
|
||||
assert.doesNotThrow(() => assertRequiredCodeExecutorAvailable('aider', {
|
||||
enabled: true,
|
||||
executors: ['aider', 'openhands'],
|
||||
}));
|
||||
assert.throws(
|
||||
() => assertRequiredCodeExecutorAvailable('aider', {
|
||||
enabled: false,
|
||||
executors: ['aider', 'openhands'],
|
||||
}),
|
||||
(error) => error?.code === 'REQUIRED_EXECUTOR_UNAVAILABLE' && error?.retryable === false,
|
||||
);
|
||||
assert.throws(
|
||||
() => assertRequiredCodeExecutorAvailable('aider', {
|
||||
enabled: true,
|
||||
executors: ['openhands'],
|
||||
}),
|
||||
(error) => error?.code === 'REQUIRED_EXECUTOR_UNAVAILABLE',
|
||||
);
|
||||
});
|
||||
|
||||
test('required image generation cannot succeed without a verified raster image_make result', () => {
|
||||
const row = {
|
||||
user_message_json: JSON.stringify({
|
||||
|
||||
+56
-3
@@ -1,4 +1,5 @@
|
||||
import { normalizeAgentRunToolMode } from './agent-run-gateway.mjs';
|
||||
import { AIDER_DEVELOPMENT_SKILL_NAME } from './chat-skills.mjs';
|
||||
import { createSessionAccess } from './session-broker.mjs';
|
||||
import {
|
||||
extractRunFromStreamEvent,
|
||||
@@ -51,6 +52,41 @@ function hasExpectedFileValidation(userMessage) {
|
||||
});
|
||||
}
|
||||
|
||||
function selectedChatSkill(userMessage) {
|
||||
const metadata = userMessage?.metadata;
|
||||
const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {};
|
||||
return String(runMetadata.selectedChatSkill ?? '').trim();
|
||||
}
|
||||
|
||||
export function enforceSelectedSkillRuntime(userMessage, {
|
||||
rawToolMode = 'chat',
|
||||
taskType = null,
|
||||
} = {}) {
|
||||
if (selectedChatSkill(userMessage) !== AIDER_DEVELOPMENT_SKILL_NAME) {
|
||||
return { userMessage, rawToolMode, taskType, requiredExecutor: null };
|
||||
}
|
||||
const message = userMessage && typeof userMessage === 'object' && !Array.isArray(userMessage)
|
||||
? { ...userMessage }
|
||||
: { value: userMessage };
|
||||
const metadata = message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata)
|
||||
? { ...message.metadata }
|
||||
: {};
|
||||
const runMetadata = metadata.memindRun && typeof metadata.memindRun === 'object' && !Array.isArray(metadata.memindRun)
|
||||
? { ...metadata.memindRun }
|
||||
: {};
|
||||
metadata.memindRun = {
|
||||
...runMetadata,
|
||||
selectedChatSkill: AIDER_DEVELOPMENT_SKILL_NAME,
|
||||
executor: 'aider',
|
||||
};
|
||||
return {
|
||||
userMessage: { ...message, metadata },
|
||||
rawToolMode: 'code',
|
||||
taskType: 'h5_chat_code_task',
|
||||
requiredExecutor: 'aider',
|
||||
};
|
||||
}
|
||||
|
||||
export function createPostAgentRunsHandler({
|
||||
userAuth,
|
||||
sessionAccess = null,
|
||||
@@ -86,9 +122,9 @@ export function createPostAgentRunsHandler({
|
||||
try {
|
||||
const sessionId = String(request.body?.session_id ?? '').trim() || null;
|
||||
const requestId = String(request.body?.request_id ?? '').trim();
|
||||
const userMessage = request.body?.user_message ?? null;
|
||||
const rawToolMode = request.body?.tool_mode ?? request.body?.toolMode ?? 'chat';
|
||||
const taskType = String(request.body?.task_type ?? request.body?.taskType ?? '').trim() || null;
|
||||
let userMessage = request.body?.user_message ?? null;
|
||||
let rawToolMode = request.body?.tool_mode ?? request.body?.toolMode ?? 'chat';
|
||||
let taskType = String(request.body?.task_type ?? request.body?.taskType ?? '').trim() || null;
|
||||
const forceDeepReasoning = request.body?.force_deep_reasoning === true || request.body?.forceDeepReasoning === true;
|
||||
if (!requestId) {
|
||||
response.status(400).json({ message: '缺少 request_id' });
|
||||
@@ -98,6 +134,23 @@ export function createPostAgentRunsHandler({
|
||||
response.status(400).json({ message: '缺少 user_message' });
|
||||
return;
|
||||
}
|
||||
const selectedSkillRuntime = enforceSelectedSkillRuntime(userMessage, {
|
||||
rawToolMode,
|
||||
taskType,
|
||||
});
|
||||
userMessage = selectedSkillRuntime.userMessage;
|
||||
rawToolMode = selectedSkillRuntime.rawToolMode;
|
||||
taskType = selectedSkillRuntime.taskType;
|
||||
if (
|
||||
selectedSkillRuntime.requiredExecutor &&
|
||||
userAuth?.getUserSkills
|
||||
) {
|
||||
const skillState = await userAuth.getUserSkills(request.currentUser.id);
|
||||
if (!skillState?.skills?.[AIDER_DEVELOPMENT_SKILL_NAME]) {
|
||||
response.status(403).json({ message: '当前用户未授权 Aider 开发技能' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
let toolMode = 'chat';
|
||||
try {
|
||||
toolMode = normalizeAgentRunToolMode(rawToolMode);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createAgentRunEventsHandler,
|
||||
createGetAgentRunHandler,
|
||||
createPostAgentRunsHandler,
|
||||
enforceSelectedSkillRuntime,
|
||||
} from './agent-run-routes.mjs';
|
||||
|
||||
function createResponseRecorder() {
|
||||
@@ -100,6 +101,102 @@ test('POST /agent/runs creates a run and returns 202', async () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('selected Aider development skill forces code mode, task type, and Aider executor', async () => {
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '修改页面并测试' }],
|
||||
metadata: {
|
||||
memindRun: {
|
||||
selectedChatSkill: 'aider-development',
|
||||
executor: 'openhands',
|
||||
validation: { expectedFile: '.memind/agent-runs/req-aider.json' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const enforced = enforceSelectedSkillRuntime(userMessage, {
|
||||
rawToolMode: 'chat',
|
||||
taskType: 'page_data_dev_complex',
|
||||
});
|
||||
assert.equal(enforced.rawToolMode, 'code');
|
||||
assert.equal(enforced.taskType, 'h5_chat_code_task');
|
||||
assert.equal(enforced.requiredExecutor, 'aider');
|
||||
assert.equal(enforced.userMessage.metadata.memindRun.executor, 'aider');
|
||||
|
||||
const created = [];
|
||||
const handler = createPostAgentRunsHandler({
|
||||
userAuth: {
|
||||
async getUserSkills() {
|
||||
return { skills: { 'aider-development': true } };
|
||||
},
|
||||
},
|
||||
agentRunGateway: {
|
||||
async createRun(userId, payload) {
|
||||
created.push({ userId, payload });
|
||||
return { id: 'run-aider', status: 'queued' };
|
||||
},
|
||||
},
|
||||
codeRunsEnabled: true,
|
||||
requireCodeRunValidation: true,
|
||||
});
|
||||
const res = createResponseRecorder();
|
||||
await handler(
|
||||
{
|
||||
currentUser: { id: 'user-1' },
|
||||
body: {
|
||||
request_id: 'req-aider',
|
||||
user_message: userMessage,
|
||||
tool_mode: 'chat',
|
||||
task_type: 'page_data_dev_complex',
|
||||
},
|
||||
},
|
||||
res,
|
||||
);
|
||||
|
||||
assert.equal(res.statusCode, 202);
|
||||
assert.equal(created[0].payload.toolMode, 'code');
|
||||
assert.equal(created[0].payload.taskType, 'h5_chat_code_task');
|
||||
assert.equal(created[0].payload.userMessage.metadata.memindRun.executor, 'aider');
|
||||
});
|
||||
|
||||
test('selected Aider development skill fails closed when code runs are disabled', async () => {
|
||||
const handler = createPostAgentRunsHandler({
|
||||
userAuth: {
|
||||
async getUserSkills() {
|
||||
return { skills: { 'aider-development': true } };
|
||||
},
|
||||
},
|
||||
agentRunGateway: {
|
||||
async createRun() {
|
||||
throw new Error('must not fall back to a normal run');
|
||||
},
|
||||
},
|
||||
codeRunsEnabled: false,
|
||||
});
|
||||
const res = createResponseRecorder();
|
||||
await handler(
|
||||
{
|
||||
currentUser: { id: 'user-1' },
|
||||
body: {
|
||||
request_id: 'req-aider-disabled',
|
||||
user_message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '修复代码' }],
|
||||
metadata: {
|
||||
memindRun: {
|
||||
selectedChatSkill: 'aider-development',
|
||||
validation: { expectedFile: '.memind/agent-runs/req-aider-disabled.json' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res,
|
||||
);
|
||||
|
||||
assert.equal(res.statusCode, 403);
|
||||
assert.match(res.body.message, /代码任务灰度未开启/);
|
||||
});
|
||||
|
||||
test('POST /agent/runs forwards deep reasoning flag to the run gateway', async () => {
|
||||
const created = [];
|
||||
const handler = createPostAgentRunsHandler({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const PUBLISH_SKILL_NAME = 'static-page-publish';
|
||||
export const PAGE_DATA_COLLECT_SKILL_NAME = 'page-data-collect';
|
||||
export const EXCEL_ANALYST_SKILL_NAME = 'excel-analyst';
|
||||
export const AIDER_DEVELOPMENT_SKILL_NAME = 'aider-development';
|
||||
export const SKILL_ROUTER_V2_ENV = 'TKMIND_SKILL_ROUTER_V2';
|
||||
|
||||
const EXCEL_ANALYSIS_INTENT_PATTERNS = [
|
||||
@@ -171,6 +172,14 @@ export const CHAT_SKILL_DEFINITIONS = [
|
||||
prefillOnly: true,
|
||||
promptKey: 'service-integration-smoke',
|
||||
},
|
||||
{
|
||||
id: AIDER_DEVELOPMENT_SKILL_NAME,
|
||||
label: 'Aider 开发',
|
||||
icon: 'spark',
|
||||
skillName: AIDER_DEVELOPMENT_SKILL_NAME,
|
||||
requiresSkill: AIDER_DEVELOPMENT_SKILL_NAME,
|
||||
promptKey: AIDER_DEVELOPMENT_SKILL_NAME,
|
||||
},
|
||||
{
|
||||
id: 'form-collect',
|
||||
label: '表单收集',
|
||||
@@ -251,6 +260,11 @@ export function buildChatSkillPrompt(promptKey, skillName) {
|
||||
);
|
||||
case 'service-integration-smoke':
|
||||
return `请使用 ${skillName ?? 'service-integration-smoke'} 技能:按标准联调流程检查当前服务,覆盖身份、普通聊天、记忆读取,以及我本轮明确要求验证的技能/发布链路,并输出通过项、失败项、待确认项:`;
|
||||
case AIDER_DEVELOPMENT_SKILL_NAME:
|
||||
return (
|
||||
`请使用 ${skillName ?? AIDER_DEVELOPMENT_SKILL_NAME} 技能:` +
|
||||
'本轮必须由 Aider code run 在当前用户工作区执行开发、修复和验证;Aider 或 Tool Gateway 不可用时直接失败,禁止回退到 Goose、OpenHands 或普通聊天冒充执行。我的开发任务是:'
|
||||
);
|
||||
case 'table-viewer':
|
||||
return `请使用 ${skillName ?? 'table-viewer'} 技能:请把以下数据整理成可排序、可筛选的交互式表格:`;
|
||||
case 'product-campaign-page':
|
||||
|
||||
@@ -47,6 +47,19 @@ test('filterChatSkills shows service integration smoke when granted', () => {
|
||||
assert.ok(visible.some((item) => item.id === 'service-integration-smoke'));
|
||||
});
|
||||
|
||||
test('filterChatSkills only shows Aider development when granted', () => {
|
||||
const hidden = filterChatSkills(CHAT_SKILL_DEFINITIONS, {
|
||||
canPublish: false,
|
||||
grantedSkills: [],
|
||||
});
|
||||
assert.equal(hidden.some((item) => item.id === 'aider-development'), false);
|
||||
const visible = filterChatSkills(CHAT_SKILL_DEFINITIONS, {
|
||||
canPublish: false,
|
||||
grantedSkills: ['aider-development'],
|
||||
});
|
||||
assert.ok(visible.some((item) => item.id === 'aider-development'));
|
||||
});
|
||||
|
||||
test('filterChatSkills shows page-data-collect when granted without static publish', () => {
|
||||
const visible = filterChatSkills(CHAT_SKILL_DEFINITIONS, {
|
||||
canPublish: false,
|
||||
@@ -69,6 +82,8 @@ test('buildChatSkillPrompt includes skill name for platform skills', () => {
|
||||
assert.match(enhancedPrompt, /tkmind_search/);
|
||||
assert.match(enhancedPrompt, /web_search/);
|
||||
assert.match(buildChatSkillPrompt('service-integration-smoke'), /标准联调流程/);
|
||||
assert.match(buildChatSkillPrompt('aider-development'), /必须由 Aider code run/);
|
||||
assert.match(buildChatSkillPrompt('aider-development'), /禁止回退/);
|
||||
assert.match(buildChatSkillPrompt('product-campaign-page'), /商品宣传 \/ 活动页/);
|
||||
assert.match(buildChatSkillPrompt('image-generation'), /asset\.htmlSrc/);
|
||||
assert.match(buildChatSkillPrompt('image-generation'), /workspaceRelativePath 只用于文件操作/);
|
||||
|
||||
@@ -13,6 +13,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const PAGE_DATA_COLLECT_SKILL_NAME = 'page-data-collect';
|
||||
export const EXCEL_ANALYST_SKILL_NAME = 'excel-analyst';
|
||||
export const IMAGE_GENERATION_SKILL_NAME = 'image-generation';
|
||||
export const AIDER_DEVELOPMENT_SKILL_NAME = 'aider-development';
|
||||
|
||||
export const DEFAULT_USER_SKILLS = {
|
||||
web: true,
|
||||
@@ -28,6 +29,7 @@ export const DEFAULT_USER_SKILLS = {
|
||||
'long-image-download': true,
|
||||
[IMAGE_GENERATION_SKILL_NAME]: true,
|
||||
[PAGE_DATA_COLLECT_SKILL_NAME]: true,
|
||||
[AIDER_DEVELOPMENT_SKILL_NAME]: false,
|
||||
[PUBLISH_SKILL_NAME]: false,
|
||||
};
|
||||
|
||||
@@ -42,6 +44,7 @@ export const USER_ROLE_SKILL_PRESETS = {
|
||||
},
|
||||
developer: {
|
||||
...DEFAULT_USER_SKILLS,
|
||||
[AIDER_DEVELOPMENT_SKILL_NAME]: true,
|
||||
git: true,
|
||||
'diff-viewer': true,
|
||||
'code-playground': true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
listPlatformSkillCatalog,
|
||||
normalizeSkillPatch,
|
||||
resolveSkillMap,
|
||||
USER_ROLE_SKILL_PRESETS,
|
||||
} from './skills-registry.mjs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -25,6 +26,9 @@ test('lists static-page-publish in platform catalog', () => {
|
||||
assert.ok(imageGeneration.manifest.trigger.keywords.includes('AI配图'));
|
||||
assert.equal(imageGeneration.manifest.router.promptKey, 'image-generation');
|
||||
assert.ok(catalog.some((item) => item.name === 'excel-analyst'));
|
||||
const aiderDevelopment = catalog.find((item) => item.name === 'aider-development');
|
||||
assert.ok(aiderDevelopment);
|
||||
assert.deepEqual(aiderDevelopment.executors, ['aider']);
|
||||
});
|
||||
|
||||
test('granting page-data-collect enables static_publish capability', () => {
|
||||
@@ -73,4 +77,6 @@ test('DEFAULT_USER_SKILLS enables common platform skills', () => {
|
||||
assert.equal(DEFAULT_USER_SKILLS['page-data-collect'], true);
|
||||
assert.equal(DEFAULT_USER_SKILLS['static-page-publish'], false);
|
||||
assert.equal(DEFAULT_USER_SKILLS['excel-analyst'], false);
|
||||
assert.equal(DEFAULT_USER_SKILLS['aider-development'], false);
|
||||
assert.equal(USER_ROLE_SKILL_PRESETS.developer['aider-development'], true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: aider-development
|
||||
description: Force development, debugging, refactoring, and code validation tasks in the current MindSpace workspace to run through the Aider executor. Use only when the user explicitly selects the Aider 开发 skill or asks to continue a task already started with that selected skill; fail closed when Aider or the code-run gateway is unavailable.
|
||||
---
|
||||
|
||||
# Aider 开发
|
||||
|
||||
必须通过平台注入的 Aider code run 执行当前工作区内的开发任务。不得切换到 Goose、OpenHands 或普通聊天执行器。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 先检查任务相关文件、现有实现和验证入口,限制修改范围。
|
||||
2. 实现用户要求的代码或页面修改,保留无关文件和现有数据。
|
||||
3. 执行与改动对应的最小测试、构建或静态验证。
|
||||
4. 按运行时注入的 `[Memind code-run validation]` 要求写入验收 receipt。
|
||||
5. 只有产物与验证都成功后才报告完成;失败时返回真实错误和未完成项。
|
||||
|
||||
## 边界
|
||||
|
||||
- 只操作当前用户被授权的工作区,不越界访问其它用户或平台生产目录。
|
||||
- 不执行发布、推送、合并主线或生产变更,除非用户另行明确授权且平台闸门允许。
|
||||
- Aider 没有 `private_data_*` 能力时,不得伪造建表、dataset 注册或页面绑定结果;应明确报告需转交 `page-data-collect`/Goose 的步骤。
|
||||
- Aider、模型绑定或 Tool Gateway 不可用时必须失败关闭,不得退回普通 Agent 冒充 Aider 完成。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Aider 开发"
|
||||
short_description: "强制使用 Aider 在用户工作区内开发、修复并验证代码"
|
||||
default_prompt: "使用 $aider-development 在当前工作区实现并验证这项开发任务。"
|
||||
@@ -0,0 +1,6 @@
|
||||
name: aider-development
|
||||
version: 1.0.0
|
||||
label: Aider 开发
|
||||
description: 强制使用 Aider 在当前用户工作区开发、修复并验证代码
|
||||
executors:
|
||||
- aider
|
||||
+3
-2
@@ -174,7 +174,7 @@ function withAgentRunValidationMetadata(
|
||||
function prepareAgentRunUserMessage(message: Message, options: AgentRunCreateOptions): Message {
|
||||
const normalized = normalizeUserMessageForApi(message);
|
||||
const withValidation = withAgentRunValidationMetadata(normalized, options.validation);
|
||||
const withRunMetadata = options.forceDeepReasoning
|
||||
const withRunMetadata = options.forceDeepReasoning || options.executor
|
||||
? {
|
||||
...withValidation,
|
||||
metadata: {
|
||||
@@ -186,7 +186,8 @@ function prepareAgentRunUserMessage(message: Message, options: AgentRunCreateOpt
|
||||
? withValidation.metadata.memindRun
|
||||
: {}
|
||||
),
|
||||
forceDeepReasoning: true,
|
||||
...(options.forceDeepReasoning ? { forceDeepReasoning: true } : {}),
|
||||
...(options.executor ? { executor: options.executor } : {}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ import type {
|
||||
} from '../types';
|
||||
import { buildContextPrefix } from '../utils/mindspaceChatContext';
|
||||
import { buildUserAddressPrefix } from '../utils/userAddress';
|
||||
import { buildAutoChatSkillPrefix } from '../../chat-skills.mjs';
|
||||
import {
|
||||
AIDER_DEVELOPMENT_SKILL_NAME,
|
||||
buildAutoChatSkillPrefix,
|
||||
} from '../../chat-skills.mjs';
|
||||
import {
|
||||
reconcileSessionEventRequestContext,
|
||||
resolvePostAgentRunChatState,
|
||||
@@ -1473,8 +1476,11 @@ export function useTKMindChat(
|
||||
|
||||
try {
|
||||
const pageDataDevTaskType = resolvePageDataDevTaskType(trimmed);
|
||||
const requiresAider = options?.selectedChatSkill === AIDER_DEVELOPMENT_SKILL_NAME;
|
||||
const runOptions = resolveAgentRunOptions(trimmed, {
|
||||
taskType: pageDataDevTaskType ?? 'h5_chat_code_task',
|
||||
forceCode: requiresAider,
|
||||
requiredExecutor: requiresAider ? 'aider' : undefined,
|
||||
userId: userRef.current?.id ?? null,
|
||||
requestId,
|
||||
mindspaceContext: options?.mindspaceContext ?? null,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { MindSpaceChatContext, AgentCodeRunClientPolicy } from '../types';
|
||||
export type AgentRunCreateOptions = {
|
||||
toolMode?: 'chat' | 'code';
|
||||
taskType?: string | null;
|
||||
executor?: 'aider' | 'openhands';
|
||||
forceDeepReasoning?: boolean;
|
||||
validation?: AgentRunValidation | null;
|
||||
validationInstruction?: string | null;
|
||||
@@ -299,6 +300,7 @@ export function resolveAgentRunOptions(
|
||||
}: {
|
||||
taskType?: string;
|
||||
forceCode?: boolean;
|
||||
requiredExecutor?: 'aider' | 'openhands';
|
||||
allowAutodetect?: boolean;
|
||||
allowPageDataDevAutodetect?: boolean;
|
||||
userId?: string | null;
|
||||
@@ -313,15 +315,17 @@ export function resolveAgentRunOptions(
|
||||
const effectiveTaskType = pageDataDevTaskType ?? taskType;
|
||||
const shouldUseDeepReasoning =
|
||||
forceCode ||
|
||||
Boolean(requiredExecutor) ||
|
||||
Boolean(pageDataDevTaskType) ||
|
||||
DEEP_REASONING_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText));
|
||||
|
||||
if (!agentCodeRunsEnabledForUser(userId)) {
|
||||
if (!agentCodeRunsEnabledForUser(userId) && !requiredExecutor) {
|
||||
return shouldUseDeepReasoning ? { forceDeepReasoning: true, taskType: effectiveTaskType } : {};
|
||||
}
|
||||
|
||||
const shouldUseCode =
|
||||
forceCode ||
|
||||
Boolean(requiredExecutor) ||
|
||||
Boolean(pageDataDevTaskType) ||
|
||||
(allowAutodetect && CODE_TASK_PATTERNS.some((pattern) => pattern.test(normalizedText)));
|
||||
if (!shouldUseCode) {
|
||||
@@ -339,6 +343,7 @@ export function resolveAgentRunOptions(
|
||||
return {
|
||||
toolMode: 'code',
|
||||
taskType: effectiveTaskType,
|
||||
...(requiredExecutor ? { executor: requiredExecutor } : {}),
|
||||
forceDeepReasoning: true,
|
||||
validation: mergeAgentRunValidationFiles(receipt.validation, taskValidation.validation),
|
||||
validationInstruction: `${receipt.instruction}${taskValidation.instruction}`,
|
||||
|
||||
@@ -43,6 +43,21 @@ test('tool gateway selects openhands for page_data_dev_complex by default', () =
|
||||
assert.equal(gateway.selectExecutor({ taskType: 'page_data_dev_complex' }), 'openhands');
|
||||
});
|
||||
|
||||
test('tool gateway honors an explicitly required Aider executor over task defaults', () => {
|
||||
const gateway = createToolGateway({ env: {} });
|
||||
assert.equal(gateway.selectExecutor({
|
||||
taskType: 'page_data_dev_complex',
|
||||
userMessage: {
|
||||
metadata: {
|
||||
memindRun: {
|
||||
executor: 'aider',
|
||||
selectedChatSkill: 'aider-development',
|
||||
},
|
||||
},
|
||||
},
|
||||
}), 'aider');
|
||||
});
|
||||
|
||||
test('tool gateway dry run builds executor launch plan without spawning', async () => {
|
||||
const plans = [];
|
||||
const gateway = createToolGateway({
|
||||
|
||||
Reference in New Issue
Block a user