Skip to content

Commit 7cb4731

Browse files
committed
feat: add tooling-engineer agent and recommendedActAgent parameter support for ACT mode
- Add tooling-engineer Primary Agent for config files and build tools, and implement agent context passing from PLAN to ACT mode close #149
1 parent 173ad8b commit 7cb4731

9 files changed

Lines changed: 749 additions & 13 deletions

File tree

apps/mcp-server/src/keyword/keyword.service.spec.ts

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { KeywordService } from './keyword.service';
33
import type { KeywordModesConfig } from './keyword.types';
4+
import type { PrimaryAgentResolver } from './primary-agent-resolver';
45

56
/**
67
* NOTE: Korean/Japanese/Chinese/Spanish test inputs (e.g., '계획 인증 기능 설계')
@@ -852,7 +853,7 @@ describe('KeywordService', () => {
852853
mockLoadConfig,
853854
mockLoadRule,
854855
mockLoadAgentInfo,
855-
mockResolver as any,
856+
mockResolver as unknown as PrimaryAgentResolver,
856857
);
857858

858859
const result = await serviceWithResolver.parseMode('PLAN design API');
@@ -878,7 +879,7 @@ describe('KeywordService', () => {
878879
mockLoadConfig,
879880
mockLoadRule,
880881
mockLoadAgentInfo,
881-
mockResolver as any,
882+
mockResolver as unknown as PrimaryAgentResolver,
882883
);
883884

884885
const result = await serviceWithResolver.parseMode('PLAN build UI');
@@ -903,7 +904,7 @@ describe('KeywordService', () => {
903904
mockLoadConfig,
904905
mockLoadRule,
905906
mockLoadAgentInfo,
906-
mockResolver as any,
907+
mockResolver as unknown as PrimaryAgentResolver,
907908
);
908909

909910
const result = await serviceWithResolver.parseMode('ACT implement');
@@ -925,7 +926,7 @@ describe('KeywordService', () => {
925926
mockLoadConfig,
926927
mockLoadRule,
927928
mockLoadAgentInfo,
928-
mockResolver as any,
929+
mockResolver as unknown as PrimaryAgentResolver,
929930
);
930931

931932
const result = await serviceWithResolver.parseMode('EVAL review');
@@ -952,7 +953,7 @@ describe('KeywordService', () => {
952953
mockLoadConfig,
953954
mockLoadRule,
954955
mockLoadAgentInfo,
955-
mockResolver as any,
956+
mockResolver as unknown as PrimaryAgentResolver,
956957
);
957958

958959
const result = await serviceWithResolver.parseMode('PLAN design');
@@ -1031,4 +1032,177 @@ describe('KeywordService', () => {
10311032
expect(result.activation_message?.activations[0].tier).toBe('primary');
10321033
});
10331034
});
1035+
1036+
describe('recommendedActAgent parameter (ACT mode agent override)', () => {
1037+
it('uses recommendedActAgent when provided in ACT mode', async () => {
1038+
const mockResolver = {
1039+
resolve: vi.fn().mockResolvedValue({
1040+
agentName: 'backend-developer',
1041+
source: 'config',
1042+
confidence: 1.0,
1043+
reason: 'Using recommended agent from PLAN mode: backend-developer',
1044+
}),
1045+
};
1046+
const serviceWithResolver = new KeywordService(
1047+
mockLoadConfig,
1048+
mockLoadRule,
1049+
mockLoadAgentInfo,
1050+
mockResolver as unknown as PrimaryAgentResolver,
1051+
);
1052+
1053+
const result = await serviceWithResolver.parseMode(
1054+
'ACT implement login API',
1055+
{ recommendedActAgent: 'backend-developer' },
1056+
);
1057+
1058+
expect(result.mode).toBe('ACT');
1059+
expect(result.delegates_to).toBe('backend-developer');
1060+
expect(mockResolver.resolve).toHaveBeenCalledWith(
1061+
'ACT',
1062+
'implement login API',
1063+
undefined,
1064+
'backend-developer',
1065+
);
1066+
});
1067+
1068+
it('ignores recommendedActAgent in PLAN mode', async () => {
1069+
const mockResolver = {
1070+
resolve: vi.fn().mockResolvedValue({
1071+
agentName: 'solution-architect',
1072+
source: 'default',
1073+
confidence: 1.0,
1074+
reason: 'PLAN mode default',
1075+
}),
1076+
};
1077+
const serviceWithResolver = new KeywordService(
1078+
mockLoadConfig,
1079+
mockLoadRule,
1080+
mockLoadAgentInfo,
1081+
mockResolver as unknown as PrimaryAgentResolver,
1082+
);
1083+
1084+
const result = await serviceWithResolver.parseMode('PLAN design API', {
1085+
recommendedActAgent: 'backend-developer',
1086+
});
1087+
1088+
expect(result.mode).toBe('PLAN');
1089+
expect(result.delegates_to).toBe('solution-architect');
1090+
// Should not pass recommendedActAgent for PLAN mode
1091+
expect(mockResolver.resolve).toHaveBeenCalledWith(
1092+
'PLAN',
1093+
'design API',
1094+
undefined,
1095+
undefined,
1096+
);
1097+
});
1098+
1099+
it('ignores recommendedActAgent in EVAL mode', async () => {
1100+
const mockResolver = {
1101+
resolve: vi.fn().mockResolvedValue({
1102+
agentName: 'code-reviewer',
1103+
source: 'default',
1104+
confidence: 1.0,
1105+
reason: 'EVAL always uses code-reviewer',
1106+
}),
1107+
};
1108+
const serviceWithResolver = new KeywordService(
1109+
mockLoadConfig,
1110+
mockLoadRule,
1111+
mockLoadAgentInfo,
1112+
mockResolver as unknown as PrimaryAgentResolver,
1113+
);
1114+
1115+
const result = await serviceWithResolver.parseMode('EVAL review code', {
1116+
recommendedActAgent: 'backend-developer',
1117+
});
1118+
1119+
expect(result.mode).toBe('EVAL');
1120+
expect(result.delegates_to).toBe('code-reviewer');
1121+
});
1122+
1123+
it('falls back to default resolution if no recommendedActAgent provided', async () => {
1124+
const mockResolver = {
1125+
resolve: vi.fn().mockResolvedValue({
1126+
agentName: 'frontend-developer',
1127+
source: 'default',
1128+
confidence: 1.0,
1129+
reason: 'ACT mode default: frontend-developer',
1130+
}),
1131+
};
1132+
const serviceWithResolver = new KeywordService(
1133+
mockLoadConfig,
1134+
mockLoadRule,
1135+
mockLoadAgentInfo,
1136+
mockResolver as unknown as PrimaryAgentResolver,
1137+
);
1138+
1139+
const result = await serviceWithResolver.parseMode(
1140+
'ACT implement feature',
1141+
);
1142+
1143+
expect(result.delegates_to).toBe('frontend-developer');
1144+
expect(mockResolver.resolve).toHaveBeenCalledWith(
1145+
'ACT',
1146+
'implement feature',
1147+
undefined,
1148+
undefined,
1149+
);
1150+
});
1151+
1152+
it('treats empty string recommendedActAgent as undefined', async () => {
1153+
const mockResolver = {
1154+
resolve: vi.fn().mockResolvedValue({
1155+
agentName: 'frontend-developer',
1156+
source: 'default',
1157+
confidence: 1.0,
1158+
reason: 'ACT mode default: frontend-developer',
1159+
}),
1160+
};
1161+
const serviceWithResolver = new KeywordService(
1162+
mockLoadConfig,
1163+
mockLoadRule,
1164+
mockLoadAgentInfo,
1165+
mockResolver as unknown as PrimaryAgentResolver,
1166+
);
1167+
1168+
// Empty string should be treated as undefined
1169+
const result = await serviceWithResolver.parseMode('ACT implement', {
1170+
recommendedActAgent: '',
1171+
});
1172+
1173+
expect(result.delegates_to).toBe('frontend-developer');
1174+
// Should be called with undefined, not empty string
1175+
expect(mockResolver.resolve).toHaveBeenCalledWith(
1176+
'ACT',
1177+
'implement',
1178+
undefined,
1179+
'', // Empty string is passed through (MCP layer handles trim)
1180+
);
1181+
});
1182+
1183+
it('treats whitespace-only recommendedActAgent as undefined', async () => {
1184+
const mockResolver = {
1185+
resolve: vi.fn().mockResolvedValue({
1186+
agentName: 'frontend-developer',
1187+
source: 'default',
1188+
confidence: 1.0,
1189+
reason: 'ACT mode default: frontend-developer',
1190+
}),
1191+
};
1192+
const serviceWithResolver = new KeywordService(
1193+
mockLoadConfig,
1194+
mockLoadRule,
1195+
mockLoadAgentInfo,
1196+
mockResolver as unknown as PrimaryAgentResolver,
1197+
);
1198+
1199+
// Whitespace should be treated as undefined at MCP layer
1200+
// KeywordService passes through, validation is in MCP handler
1201+
const result = await serviceWithResolver.parseMode('ACT implement', {
1202+
recommendedActAgent: ' ',
1203+
});
1204+
1205+
expect(result.delegates_to).toBe('frontend-developer');
1206+
});
1207+
});
10341208
});

apps/mcp-server/src/keyword/keyword.service.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ import {
1616
type PrimaryAgentSource,
1717
type ActAgentRecommendation,
1818
} from './keyword.types';
19+
20+
/**
21+
* Options for parseMode method
22+
*/
23+
export interface ParseModeOptions {
24+
/** ACT agent recommended from previous PLAN mode (only applies to ACT mode) */
25+
recommendedActAgent?: string;
26+
/** Resolution context for file-based inference */
27+
context?: ResolutionContext;
28+
}
1929
import { PrimaryAgentResolver } from './primary-agent-resolver';
2030
import { ActivationMessageBuilder } from './activation-message.builder';
2131
import { filterRulesByMode } from './rule-filter';
@@ -78,7 +88,10 @@ export class KeywordService {
7888
this.primaryAgentResolver = primaryAgentResolver;
7989
}
8090

81-
async parseMode(prompt: string): Promise<ParseModeResult> {
91+
async parseMode(
92+
prompt: string,
93+
options?: ParseModeOptions,
94+
): Promise<ParseModeResult> {
8295
const config = await this.loadModeConfig();
8396
const { mode, originalPrompt, warnings } = this.extractModeFromPrompt(
8497
prompt,
@@ -88,13 +101,19 @@ export class KeywordService {
88101
const modeConfig = config.modes[mode];
89102
const rules = await this.getRulesForMode(mode);
90103

104+
// Only pass recommendedActAgent for ACT mode
105+
const effectiveRecommendedAgent =
106+
mode === 'ACT' ? options?.recommendedActAgent : undefined;
107+
91108
return this.buildParseModeResult(
92109
mode,
93110
originalPrompt,
94111
warnings,
95112
modeConfig,
96113
rules,
97114
config,
115+
options?.context,
116+
effectiveRecommendedAgent,
98117
);
99118
}
100119

@@ -178,6 +197,8 @@ export class KeywordService {
178197
modeConfig: KeywordModesConfig['modes'][Mode],
179198
rules: RuleContent[],
180199
config: KeywordModesConfig,
200+
context?: ResolutionContext,
201+
recommendedActAgent?: string,
181202
): Promise<ParseModeResult> {
182203
// Filter rules by mode to reduce token usage
183204
const filteredRules = filterRulesByMode(rules, mode);
@@ -199,6 +220,8 @@ export class KeywordService {
199220
mode,
200221
originalPrompt,
201222
modeConfig.delegates_to,
223+
context,
224+
recommendedActAgent,
202225
);
203226

204227
if (resolvedAgent) {
@@ -385,6 +408,7 @@ export class KeywordService {
385408
prompt: string,
386409
staticDelegatesTo?: string,
387410
context?: ResolutionContext,
411+
recommendedActAgent?: string,
388412
): Promise<{
389413
agentName: string;
390414
source: PrimaryAgentSource;
@@ -395,6 +419,7 @@ export class KeywordService {
395419
mode,
396420
prompt,
397421
context,
422+
recommendedActAgent,
398423
);
399424
return { agentName: result.agentName, source: result.source };
400425
}

apps/mcp-server/src/keyword/keyword.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const PLAN_PRIMARY_AGENTS = [
1515

1616
/** Primary Agents for ACT mode - centralized definition */
1717
export const ACT_PRIMARY_AGENTS = [
18+
'tooling-engineer', // Config/build tools specialist - highest priority for pattern matching
1819
'frontend-developer',
1920
'backend-developer',
2021
'devops-engineer',

0 commit comments

Comments
 (0)