aiexecode 1.0.92 → 1.0.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/README.md +210 -87
- package/index.js +33 -1
- package/package.json +3 -3
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +3 -3
- package/payload_viewer/web_server.js +361 -0
- package/src/LLMClient/client.js +392 -16
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +608 -0
- package/src/LLMClient/errors.js +18 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +35 -4
- package/src/ai_based/orchestrator.js +146 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/commands.js +51 -0
- package/src/commands/debug.js +52 -0
- package/src/commands/help.js +11 -1
- package/src/commands/model.js +43 -7
- package/src/commands/skills.js +46 -0
- package/src/config/ai_models.js +96 -5
- package/src/config/constants.js +71 -0
- package/src/frontend/App.js +4 -5
- package/src/frontend/components/ConversationItem.js +25 -24
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/SetupWizard.js +53 -8
- package/src/frontend/utils/syntaxHighlighter.js +4 -4
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +289 -0
- package/src/system/ai_request.js +147 -9
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/session.js +59 -35
- package/src/system/skill_loader.js +318 -0
- package/src/system/tool_approval.js +10 -0
- package/src/tools/file_reader.js +49 -9
- package/src/tools/glob.js +0 -3
- package/src/tools/ripgrep.js +5 -7
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/web_downloader.js +0 -3
- package/src/util/clone.js +174 -0
- package/src/util/config.js +38 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +68 -1
- package/src/util/safe_fs.js +43 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{d0-fu2rgYnshgGFPxr1CR → lHmNygVpv4N1VR0LdnwkJ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGENTS.md Loader
|
|
3
|
+
*
|
|
4
|
+
* AGENTS.md는 AI 코딩 에이전트를 위한 오픈 표준 포맷입니다.
|
|
5
|
+
* README.md가 인간을 위한 것이라면, AGENTS.md는 AI 에이전트를 위한 것입니다.
|
|
6
|
+
*
|
|
7
|
+
* 파일 위치 및 우선순위 (높은 순):
|
|
8
|
+
* 1. CWD/AGENTS.md - 현재 작업 디렉토리
|
|
9
|
+
* 2. CWD/.aiexe/AGENTS.md - 프로젝트별 설정
|
|
10
|
+
* 3. ~/.aiexe/AGENTS.md - 글로벌 설정
|
|
11
|
+
*
|
|
12
|
+
* 하위 디렉토리 지원:
|
|
13
|
+
* - 하위 디렉토리에 AGENTS.md가 있으면 해당 디렉토리 작업 시 추가로 로드
|
|
14
|
+
* - 가장 가까운 AGENTS.md가 우선순위를 가짐
|
|
15
|
+
*
|
|
16
|
+
* 참조: https://agents.md/
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { join, dirname, resolve } from 'path';
|
|
20
|
+
import { safeReadFile, safeAccess, safeStat } from '../util/safe_fs.js';
|
|
21
|
+
import { CONFIG_DIR } from '../util/config.js';
|
|
22
|
+
import { createDebugLogger } from '../util/debug_log.js';
|
|
23
|
+
|
|
24
|
+
const debugLog = createDebugLogger('agents_loader.log', 'agents_loader');
|
|
25
|
+
|
|
26
|
+
// AGENTS.md 파일명 (대소문자 구분)
|
|
27
|
+
const AGENTS_MD_FILENAME = 'AGENTS.md';
|
|
28
|
+
|
|
29
|
+
// 권장 최대 줄 수 (GitHub 분석 기반)
|
|
30
|
+
const RECOMMENDED_MAX_LINES = 150;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 파일이 존재하는지 확인합니다.
|
|
34
|
+
* @param {string} filePath - 확인할 파일 경로
|
|
35
|
+
* @returns {Promise<boolean>}
|
|
36
|
+
*/
|
|
37
|
+
async function fileExists(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
await safeAccess(filePath);
|
|
40
|
+
const stat = await safeStat(filePath);
|
|
41
|
+
return stat.isFile();
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* AGENTS.md 파일을 로드합니다.
|
|
49
|
+
* @param {string} filePath - AGENTS.md 파일 경로
|
|
50
|
+
* @returns {Promise<{path: string, content: string}|null>}
|
|
51
|
+
*/
|
|
52
|
+
async function loadAgentsMd(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
if (!await fileExists(filePath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const content = await safeReadFile(filePath, 'utf8');
|
|
58
|
+
const trimmedContent = content.trim();
|
|
59
|
+
const lineCount = trimmedContent.split('\n').length;
|
|
60
|
+
|
|
61
|
+
debugLog(`[loadAgentsMd] Loaded: ${filePath} (${trimmedContent.length} chars, ${lineCount} lines)`);
|
|
62
|
+
|
|
63
|
+
// 권장 크기 초과 시 경고
|
|
64
|
+
if (lineCount > RECOMMENDED_MAX_LINES) {
|
|
65
|
+
debugLog(`[loadAgentsMd] WARNING: ${filePath} has ${lineCount} lines (recommended: <=${RECOMMENDED_MAX_LINES})`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
path: filePath,
|
|
70
|
+
content: trimmedContent,
|
|
71
|
+
lineCount
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
debugLog(`[loadAgentsMd] Error loading ${filePath}: ${error.message}`);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 디렉토리에서 상위로 올라가며 AGENTS.md를 찾습니다.
|
|
81
|
+
* @param {string} startDir - 시작 디렉토리
|
|
82
|
+
* @param {string} [stopAt] - 여기까지만 탐색 (기본: 루트)
|
|
83
|
+
* @returns {Promise<{path: string, content: string}|null>}
|
|
84
|
+
*/
|
|
85
|
+
async function findAgentsMdUpward(startDir, stopAt = '/') {
|
|
86
|
+
let currentDir = resolve(startDir);
|
|
87
|
+
const stopDir = resolve(stopAt);
|
|
88
|
+
|
|
89
|
+
while (currentDir && currentDir !== stopDir) {
|
|
90
|
+
const agentsMdPath = join(currentDir, AGENTS_MD_FILENAME);
|
|
91
|
+
|
|
92
|
+
const result = await loadAgentsMd(agentsMdPath);
|
|
93
|
+
if (result) {
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parentDir = dirname(currentDir);
|
|
98
|
+
if (parentDir === currentDir) {
|
|
99
|
+
break; // 루트에 도달
|
|
100
|
+
}
|
|
101
|
+
currentDir = parentDir;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 모든 관련 AGENTS.md 파일을 수집합니다.
|
|
109
|
+
* 우선순위 순서로 반환 (높은 우선순위가 먼저)
|
|
110
|
+
*
|
|
111
|
+
* @param {string} [cwd] - 현재 작업 디렉토리 (기본: process.cwd())
|
|
112
|
+
* @returns {Promise<Array<{path: string, content: string, source: string}>>}
|
|
113
|
+
*/
|
|
114
|
+
export async function discoverAllAgentsMd(cwd = process.cwd()) {
|
|
115
|
+
debugLog(`[discoverAllAgentsMd] Starting discovery from: ${cwd}`);
|
|
116
|
+
|
|
117
|
+
const results = [];
|
|
118
|
+
|
|
119
|
+
// 1. CWD/AGENTS.md (최고 우선순위)
|
|
120
|
+
const cwdAgentsMd = await loadAgentsMd(join(cwd, AGENTS_MD_FILENAME));
|
|
121
|
+
if (cwdAgentsMd) {
|
|
122
|
+
results.push({
|
|
123
|
+
...cwdAgentsMd,
|
|
124
|
+
source: 'project'
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 2. CWD/.aiexe/AGENTS.md (프로젝트별 설정)
|
|
129
|
+
const projectAgentsMd = await loadAgentsMd(join(cwd, '.aiexe', AGENTS_MD_FILENAME));
|
|
130
|
+
if (projectAgentsMd) {
|
|
131
|
+
results.push({
|
|
132
|
+
...projectAgentsMd,
|
|
133
|
+
source: 'project-config'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. 상위 디렉토리 탐색 (CWD가 아닌 상위에서)
|
|
138
|
+
const parentAgentsMd = await findAgentsMdUpward(dirname(cwd));
|
|
139
|
+
if (parentAgentsMd && !results.some(r => r.path === parentAgentsMd.path)) {
|
|
140
|
+
results.push({
|
|
141
|
+
...parentAgentsMd,
|
|
142
|
+
source: 'parent'
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4. ~/.aiexe/AGENTS.md (글로벌 설정 - 가장 낮은 우선순위)
|
|
147
|
+
const globalAgentsMd = await loadAgentsMd(join(CONFIG_DIR, AGENTS_MD_FILENAME));
|
|
148
|
+
if (globalAgentsMd && !results.some(r => r.path === globalAgentsMd.path)) {
|
|
149
|
+
results.push({
|
|
150
|
+
...globalAgentsMd,
|
|
151
|
+
source: 'global'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
debugLog(`[discoverAllAgentsMd] Found ${results.length} AGENTS.md files`);
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 특정 파일/디렉토리에 가장 가까운 AGENTS.md를 찾습니다.
|
|
161
|
+
* 하위 디렉토리 작업 시 해당 디렉토리의 AGENTS.md를 우선 사용
|
|
162
|
+
*
|
|
163
|
+
* @param {string} targetPath - 대상 파일 또는 디렉토리 경로
|
|
164
|
+
* @returns {Promise<{path: string, content: string}|null>}
|
|
165
|
+
*/
|
|
166
|
+
export async function findNearestAgentsMd(targetPath) {
|
|
167
|
+
const resolvedPath = resolve(targetPath);
|
|
168
|
+
|
|
169
|
+
// 파일인지 디렉토리인지 확인
|
|
170
|
+
let startDir;
|
|
171
|
+
try {
|
|
172
|
+
const stat = await safeStat(resolvedPath);
|
|
173
|
+
startDir = stat.isDirectory() ? resolvedPath : dirname(resolvedPath);
|
|
174
|
+
} catch {
|
|
175
|
+
startDir = dirname(resolvedPath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
debugLog(`[findNearestAgentsMd] Searching from: ${startDir}`);
|
|
179
|
+
return await findAgentsMdUpward(startDir);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* AGENTS.md 내용을 시스템 프롬프트용 텍스트로 포맷팅합니다.
|
|
184
|
+
* @param {Array<{path: string, content: string, source: string}>} agentsMdFiles
|
|
185
|
+
* @returns {string}
|
|
186
|
+
*/
|
|
187
|
+
export function formatAgentsMdForPrompt(agentsMdFiles) {
|
|
188
|
+
if (!agentsMdFiles || agentsMdFiles.length === 0) {
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const sections = [];
|
|
193
|
+
|
|
194
|
+
for (const file of agentsMdFiles) {
|
|
195
|
+
const sourceLabel = {
|
|
196
|
+
'project': 'Project Root',
|
|
197
|
+
'project-config': 'Project Config (.aiexe/)',
|
|
198
|
+
'parent': 'Parent Directory',
|
|
199
|
+
'global': 'Global (~/.aiexe/)'
|
|
200
|
+
}[file.source] || file.source;
|
|
201
|
+
|
|
202
|
+
sections.push(`## AGENTS.md (${sourceLabel})\n**Path:** ${file.path}\n\n${file.content}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return `\n\n# Agent Instructions (AGENTS.md)\n\n` +
|
|
206
|
+
`The following instructions are from AGENTS.md files in the project.\n` +
|
|
207
|
+
`These provide project-specific guidance for AI coding agents.\n\n` +
|
|
208
|
+
sections.join('\n\n---\n\n') +
|
|
209
|
+
'\n';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 현재 프로젝트의 AGENTS.md를 로드하고 포맷팅합니다.
|
|
214
|
+
* 시스템 프롬프트에 추가할 때 사용
|
|
215
|
+
*
|
|
216
|
+
* @param {string} [cwd] - 현재 작업 디렉토리
|
|
217
|
+
* @returns {Promise<string>} 포맷팅된 AGENTS.md 내용 (없으면 빈 문자열)
|
|
218
|
+
*/
|
|
219
|
+
export async function loadAgentsMdForPrompt(cwd = process.cwd()) {
|
|
220
|
+
const agentsMdFiles = await discoverAllAgentsMd(cwd);
|
|
221
|
+
return formatAgentsMdForPrompt(agentsMdFiles);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* AGENTS.md 정보를 요약하여 반환합니다.
|
|
226
|
+
* /agents 명령어용
|
|
227
|
+
*
|
|
228
|
+
* @param {string} [cwd] - 현재 작업 디렉토리
|
|
229
|
+
* @returns {Promise<string>}
|
|
230
|
+
*/
|
|
231
|
+
export async function formatAgentsMdSummary(cwd = process.cwd()) {
|
|
232
|
+
const agentsMdFiles = await discoverAllAgentsMd(cwd);
|
|
233
|
+
|
|
234
|
+
if (agentsMdFiles.length === 0) {
|
|
235
|
+
return [
|
|
236
|
+
'No AGENTS.md files found.',
|
|
237
|
+
'',
|
|
238
|
+
'AGENTS.md is an open standard for guiding AI coding agents.',
|
|
239
|
+
'',
|
|
240
|
+
'File locations (in priority order):',
|
|
241
|
+
' 1. CWD/AGENTS.md - Project root',
|
|
242
|
+
' 2. CWD/.aiexe/AGENTS.md - Project config',
|
|
243
|
+
' 3. ~/.aiexe/AGENTS.md - Global config',
|
|
244
|
+
'',
|
|
245
|
+
'Create an AGENTS.md file to provide project-specific instructions.',
|
|
246
|
+
'',
|
|
247
|
+
'Recommended sections:',
|
|
248
|
+
' - Commands (build, test)',
|
|
249
|
+
' - Testing guidelines',
|
|
250
|
+
' - Project structure',
|
|
251
|
+
' - Code style',
|
|
252
|
+
' - Git workflow',
|
|
253
|
+
' - Boundaries (what not to touch)',
|
|
254
|
+
'',
|
|
255
|
+
'Learn more: https://agents.md/'
|
|
256
|
+
].join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const lines = ['AGENTS.md Files:', ''];
|
|
260
|
+
|
|
261
|
+
for (const file of agentsMdFiles) {
|
|
262
|
+
const sourceLabel = {
|
|
263
|
+
'project': 'Project Root',
|
|
264
|
+
'project-config': 'Project Config',
|
|
265
|
+
'parent': 'Parent Directory',
|
|
266
|
+
'global': 'Global'
|
|
267
|
+
}[file.source] || file.source;
|
|
268
|
+
|
|
269
|
+
const lineCount = file.lineCount || file.content.split('\n').length;
|
|
270
|
+
const charCount = file.content.length;
|
|
271
|
+
const sizeWarning = lineCount > RECOMMENDED_MAX_LINES
|
|
272
|
+
? ` (exceeds recommended ${RECOMMENDED_MAX_LINES} lines)`
|
|
273
|
+
: '';
|
|
274
|
+
|
|
275
|
+
lines.push(`${sourceLabel}:`);
|
|
276
|
+
lines.push(` Path: ${file.path}`);
|
|
277
|
+
lines.push(` Size: ${lineCount} lines, ${charCount} chars${sizeWarning}`);
|
|
278
|
+
|
|
279
|
+
// 첫 3줄 미리보기
|
|
280
|
+
const preview = file.content.split('\n').slice(0, 3).join('\n');
|
|
281
|
+
lines.push(` Preview:`);
|
|
282
|
+
lines.push(` ${preview.replace(/\n/g, '\n ')}`);
|
|
283
|
+
lines.push('');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
lines.push('Use /agents <path> to view full content of a specific file.');
|
|
287
|
+
|
|
288
|
+
return lines.join('\n');
|
|
289
|
+
}
|
package/src/system/ai_request.js
CHANGED
|
@@ -7,9 +7,30 @@ import { getReasoningModels, supportsReasoningEffort, DEFAULT_MODEL, getModelInf
|
|
|
7
7
|
import { createDebugLogger } from "../util/debug_log.js";
|
|
8
8
|
import { formatReadFileStdout } from "../util/output_formatter.js";
|
|
9
9
|
import { UnifiedLLMClient } from "../LLMClient/index.js";
|
|
10
|
+
import { uiEvents } from "./ui_events.js";
|
|
10
11
|
|
|
11
12
|
const debugLog = createDebugLogger('ai_request.log', 'ai_request');
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 안전한 JSON 파싱 헬퍼 함수
|
|
16
|
+
* 파싱 실패 시 에러를 던지지 않고 기본값 반환
|
|
17
|
+
* @param {string} jsonString - 파싱할 JSON 문자열
|
|
18
|
+
* @param {*} defaultValue - 파싱 실패 시 반환할 기본값
|
|
19
|
+
* @returns {*} 파싱된 객체 또는 기본값
|
|
20
|
+
*/
|
|
21
|
+
function safeJsonParse(jsonString, defaultValue = null) {
|
|
22
|
+
if (typeof jsonString !== 'string') {
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(jsonString);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
debugLog(`[safeJsonParse] JSON parsing failed: ${error.message}`);
|
|
29
|
+
debugLog(`[safeJsonParse] Input (first 200 chars): ${jsonString.substring(0, 200)}`);
|
|
30
|
+
return defaultValue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
// OpenAI SDK를 사용하기 위한 환경변수를 불러옵니다.
|
|
14
35
|
dotenv.config({ quiet: true });
|
|
15
36
|
|
|
@@ -55,15 +76,24 @@ async function getLLMClient() {
|
|
|
55
76
|
const currentModel = process.env.MODEL || settings?.MODEL || DEFAULT_MODEL;
|
|
56
77
|
const modelInfo = getModelInfo(currentModel);
|
|
57
78
|
const provider = modelInfo?.provider || 'openai';
|
|
79
|
+
const baseUrl = process.env.BASE_URL || settings?.BASE_URL || null;
|
|
58
80
|
|
|
59
|
-
debugLog('[getLLMClient] Initializing UnifiedLLMClient with API key
|
|
60
|
-
debugLog(`[getLLMClient] Model: ${currentModel}, Provider: ${provider}`);
|
|
61
|
-
|
|
81
|
+
debugLog('[getLLMClient] Initializing UnifiedLLMClient with API key: [PRESENT]');
|
|
82
|
+
debugLog(`[getLLMClient] Model: ${currentModel}, Provider: ${provider}, BaseURL: ${baseUrl || '(default)'}`);
|
|
83
|
+
|
|
84
|
+
const clientConfig = {
|
|
62
85
|
apiKey: process.env.API_KEY,
|
|
63
86
|
model: currentModel,
|
|
64
87
|
provider: provider,
|
|
65
88
|
logDir: PAYLOAD_LLM_LOG_DIR
|
|
66
|
-
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// 커스텀 BASE_URL이 설정되어 있으면 추가
|
|
92
|
+
if (baseUrl) {
|
|
93
|
+
clientConfig.baseUrl = baseUrl;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
llmClient = new UnifiedLLMClient(clientConfig);
|
|
67
97
|
|
|
68
98
|
debugLog('[getLLMClient] Client created successfully');
|
|
69
99
|
return llmClient;
|
|
@@ -181,6 +211,21 @@ function toAbsolutePath(anyPath) {
|
|
|
181
211
|
// 모든 AI 요청 과정에서 요청/응답 로그를 남기고 결과를 돌려줍니다.
|
|
182
212
|
export async function request(taskName, requestPayload) {
|
|
183
213
|
debugLog(`[request] ========== START: ${taskName} ==========`);
|
|
214
|
+
|
|
215
|
+
// 설정에서 API payload 표시 여부 확인
|
|
216
|
+
const settings = await loadSettings();
|
|
217
|
+
const showApiPayload = settings?.SHOW_API_PAYLOAD === true;
|
|
218
|
+
|
|
219
|
+
// 화면에 요청 시작 표시 (설정이 켜져 있을 때만)
|
|
220
|
+
if (showApiPayload) {
|
|
221
|
+
const taskDisplayNames = {
|
|
222
|
+
'orchestrator': '🤖 Orchestrator',
|
|
223
|
+
'completion_judge': '✅ Completion Judge'
|
|
224
|
+
};
|
|
225
|
+
const displayName = taskDisplayNames[taskName] || taskName;
|
|
226
|
+
uiEvents.addSystemMessage(`📡 API Request: ${displayName}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
184
229
|
const provider = await getCurrentProvider();
|
|
185
230
|
debugLog(`[request] Provider: ${provider}`);
|
|
186
231
|
|
|
@@ -218,8 +263,8 @@ export async function request(taskName, requestPayload) {
|
|
|
218
263
|
const msg = payloadCopy.input[i];
|
|
219
264
|
const { type, call_id, output } = msg;
|
|
220
265
|
if (type !== 'function_call_output') continue;
|
|
221
|
-
const parsedOutput =
|
|
222
|
-
if (!isValidJSON(parsedOutput.stdout)) {
|
|
266
|
+
const parsedOutput = safeJsonParse(output, { stdout: output });
|
|
267
|
+
if (!parsedOutput || !isValidJSON(parsedOutput.stdout)) {
|
|
223
268
|
// stdout와 stderr를 모두 포함하는 형식으로 변환
|
|
224
269
|
const hasStdout = parsedOutput.stdout;
|
|
225
270
|
const hasStderr = parsedOutput.stderr;
|
|
@@ -235,7 +280,10 @@ export async function request(taskName, requestPayload) {
|
|
|
235
280
|
msg.output = parsedOutput.stdout;
|
|
236
281
|
}
|
|
237
282
|
} else {
|
|
238
|
-
|
|
283
|
+
const parsedStdout = safeJsonParse(parsedOutput.stdout, null);
|
|
284
|
+
if (parsedStdout !== null) {
|
|
285
|
+
parsedOutput.stdout = parsedStdout;
|
|
286
|
+
}
|
|
239
287
|
if (parsedOutput.original_result) {
|
|
240
288
|
parsedOutput.stdout = ({ ...parsedOutput.original_result, ...(parsedOutput.stdout) });
|
|
241
289
|
delete parsedOutput.original_result;
|
|
@@ -496,16 +544,24 @@ export async function request(taskName, requestPayload) {
|
|
|
496
544
|
debugLog(`[request] Request prepared - logging to file`);
|
|
497
545
|
|
|
498
546
|
// 로그는 원본 포맷으로 저장 (API 호출 전)
|
|
547
|
+
const logStartTime = Date.now();
|
|
499
548
|
await logger(`${taskName}_REQ`, originalRequest, provider);
|
|
500
|
-
debugLog(`[request] Request logged - calling LLM API`);
|
|
549
|
+
debugLog(`[request] Request logged (${Date.now() - logStartTime}ms) - calling LLM API`);
|
|
501
550
|
|
|
502
551
|
// AbortController의 signal을 options로 전달
|
|
503
552
|
const requestOptions = {
|
|
504
553
|
signal: currentAbortController.signal
|
|
505
554
|
};
|
|
506
|
-
|
|
555
|
+
|
|
556
|
+
// ===== API 호출 시작 =====
|
|
557
|
+
const apiCallStartTime = Date.now();
|
|
558
|
+
debugLog(`[request] >>>>> API CALL START: ${new Date(apiCallStartTime).toISOString()}`);
|
|
507
559
|
|
|
508
560
|
response = await client.response(originalRequest, requestOptions);
|
|
561
|
+
|
|
562
|
+
const apiCallEndTime = Date.now();
|
|
563
|
+
const apiCallDuration = apiCallEndTime - apiCallStartTime;
|
|
564
|
+
debugLog(`[request] <<<<< API CALL END: ${apiCallDuration}ms (${new Date(apiCallEndTime).toISOString()})`);
|
|
509
565
|
debugLog(`[request] Response received - id: ${response?.id}, status: ${response?.status}, output items: ${response?.output?.length || 0}`);
|
|
510
566
|
|
|
511
567
|
// 원본 응답을 깊은 복사로 보존 (이후 수정으로부터 보호)
|
|
@@ -549,6 +605,88 @@ export async function request(taskName, requestPayload) {
|
|
|
549
605
|
// 로그는 원본 포맷으로 저장
|
|
550
606
|
debugLog(`[request] Logging response to file`);
|
|
551
607
|
await logger(`${taskName}_RES`, originalResponse, provider);
|
|
608
|
+
|
|
609
|
+
// 화면에 응답 결과 표시 (raw JSON)
|
|
610
|
+
// 캐시 토큰 정보 추출
|
|
611
|
+
const cacheTokens = response.usage?.cache_read_input_tokens || response.usage?.input_tokens_details?.cached_tokens || 0;
|
|
612
|
+
const inputTokens = response.usage?.input_tokens || 0;
|
|
613
|
+
const cacheRatio = inputTokens > 0 ? Math.round(cacheTokens / inputTokens * 100) : 0;
|
|
614
|
+
|
|
615
|
+
const rawOutput = {
|
|
616
|
+
status: response.status,
|
|
617
|
+
output: (response.output || []).map(o => {
|
|
618
|
+
if (o.type === 'reasoning') {
|
|
619
|
+
// thinking/reasoning 블록 표시
|
|
620
|
+
const thinking = o.content?.[0]?.thinking || '';
|
|
621
|
+
return {
|
|
622
|
+
type: 'thinking',
|
|
623
|
+
content: thinking.length > 150 ? thinking.substring(0, 150) + `... (${thinking.length} chars)` : thinking
|
|
624
|
+
};
|
|
625
|
+
} else if (o.type === 'function_call') {
|
|
626
|
+
return {
|
|
627
|
+
type: o.type,
|
|
628
|
+
name: o.name,
|
|
629
|
+
arguments: (() => {
|
|
630
|
+
try {
|
|
631
|
+
const args = JSON.parse(o.arguments || '{}');
|
|
632
|
+
// 긴 인자값은 축약
|
|
633
|
+
const truncated = {};
|
|
634
|
+
for (const [k, v] of Object.entries(args)) {
|
|
635
|
+
if (typeof v === 'string' && v.length > 100) {
|
|
636
|
+
truncated[k] = v.substring(0, 100) + `... (${v.length} chars)`;
|
|
637
|
+
} else {
|
|
638
|
+
truncated[k] = v;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return truncated;
|
|
642
|
+
} catch {
|
|
643
|
+
return o.arguments;
|
|
644
|
+
}
|
|
645
|
+
})()
|
|
646
|
+
};
|
|
647
|
+
} else if (o.type === 'message') {
|
|
648
|
+
const text = response.output_text || '';
|
|
649
|
+
return {
|
|
650
|
+
type: o.type,
|
|
651
|
+
text: text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return { type: o.type };
|
|
655
|
+
}),
|
|
656
|
+
// 토큰 사용량 정보 추가
|
|
657
|
+
tokens: {
|
|
658
|
+
input: inputTokens,
|
|
659
|
+
output: response.usage?.output_tokens || 0,
|
|
660
|
+
cached: cacheTokens > 0 ? `${cacheTokens} (${cacheRatio}%)` : 0
|
|
661
|
+
},
|
|
662
|
+
output_text: (() => {
|
|
663
|
+
const text = response.output_text || '';
|
|
664
|
+
if (taskName === 'completion_judge') {
|
|
665
|
+
// GLM 모델 대응: 텍스트 끝에 붙은 JSON 추출
|
|
666
|
+
try {
|
|
667
|
+
return JSON.parse(text);
|
|
668
|
+
} catch {
|
|
669
|
+
// 텍스트에서 JSON 추출 시도
|
|
670
|
+
const jsonMatch = text.match(/\{[^{}]*"should_complete"\s*:\s*(true|false)[^{}]*\}/);
|
|
671
|
+
if (jsonMatch) {
|
|
672
|
+
try {
|
|
673
|
+
return JSON.parse(jsonMatch[0]);
|
|
674
|
+
} catch {
|
|
675
|
+
return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
|
|
682
|
+
})()
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// 화면에 응답 내용 표시 (설정이 켜져 있을 때만)
|
|
686
|
+
if (showApiPayload) {
|
|
687
|
+
uiEvents.addSystemMessage(` └─ Response:\n${JSON.stringify(rawOutput, null, 2)}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
552
690
|
debugLog(`[request] ========== END: ${taskName} ==========`);
|
|
553
691
|
|
|
554
692
|
return response;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// 간단한 슬래시 커맨드 파서
|
|
2
2
|
// 사용자가 입력한 문자열에서 커맨드와 인자를 분리합니다.
|
|
3
3
|
|
|
4
|
+
import { loadSkillAsPrompt } from './skill_loader.js';
|
|
5
|
+
import { loadCustomCommandAsPrompt } from './custom_command_loader.js';
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* 슬래시 커맨드를 파싱합니다.
|
|
6
9
|
* @param {string} input - 사용자 입력 문자열 (예: "/help", "/exit", "/clear history")
|
|
@@ -65,6 +68,7 @@ export class CommandRegistry {
|
|
|
65
68
|
|
|
66
69
|
/**
|
|
67
70
|
* 커맨드를 실행합니다.
|
|
71
|
+
* 등록된 명령어가 없으면 스킬에서 찾아봅니다.
|
|
68
72
|
* @param {string} input - 사용자 입력
|
|
69
73
|
* @returns {Promise<any>} 커맨드 실행 결과
|
|
70
74
|
*/
|
|
@@ -77,11 +81,37 @@ export class CommandRegistry {
|
|
|
77
81
|
|
|
78
82
|
const commandConfig = this.commands.get(parsed.command);
|
|
79
83
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
84
|
+
if (commandConfig) {
|
|
85
|
+
return await commandConfig.handler(parsed.args, parsed);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 등록된 명령어가 없으면 커스텀 커맨드에서 찾아봅니다
|
|
89
|
+
const customCommandResult = await loadCustomCommandAsPrompt(parsed.command, parsed.args.join(' '));
|
|
90
|
+
|
|
91
|
+
if (customCommandResult) {
|
|
92
|
+
// 커스텀 커맨드를 찾았으면 커맨드 정보 반환
|
|
93
|
+
return {
|
|
94
|
+
type: 'custom_command',
|
|
95
|
+
commandName: customCommandResult.command.name,
|
|
96
|
+
prompt: customCommandResult.prompt,
|
|
97
|
+
command: customCommandResult.command
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 커스텀 커맨드가 없으면 스킬에서 찾아봅니다
|
|
102
|
+
const skillResult = await loadSkillAsPrompt(parsed.command, parsed.args.join(' '));
|
|
103
|
+
|
|
104
|
+
if (skillResult) {
|
|
105
|
+
// 스킬을 찾았으면 스킬 정보 반환
|
|
106
|
+
return {
|
|
107
|
+
type: 'skill',
|
|
108
|
+
skillName: skillResult.skill.name,
|
|
109
|
+
prompt: skillResult.prompt,
|
|
110
|
+
skill: skillResult.skill
|
|
111
|
+
};
|
|
82
112
|
}
|
|
83
113
|
|
|
84
|
-
|
|
114
|
+
throw new Error(`Unknown command: ${parsed.command}`);
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
/**
|