hanseol 4.3.0 → 4.5.0
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.
- package/dist/agents/index.d.ts +1 -2
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +0 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/planner/index.d.ts +0 -4
- package/dist/agents/planner/index.d.ts.map +1 -1
- package/dist/agents/planner/index.js +19 -13
- package/dist/agents/planner/index.js.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/core/auth/auth-gate.d.ts.map +1 -1
- package/dist/core/auth/auth-gate.js +3 -3
- package/dist/core/auth/auth-gate.js.map +1 -1
- package/dist/core/background-sync.d.ts +39 -0
- package/dist/core/background-sync.d.ts.map +1 -0
- package/dist/core/background-sync.js +249 -0
- package/dist/core/background-sync.js.map +1 -0
- package/dist/core/compact/compact-manager.d.ts.map +1 -1
- package/dist/core/compact/compact-manager.js +4 -2
- package/dist/core/compact/compact-manager.js.map +1 -1
- package/dist/core/config/config-manager.js +1 -1
- package/dist/core/config/config-manager.js.map +1 -1
- package/dist/core/docs-manager.js +3 -3
- package/dist/core/docs-manager.js.map +1 -1
- package/dist/core/llm/llm-client.d.ts +2 -0
- package/dist/core/llm/llm-client.d.ts.map +1 -1
- package/dist/core/llm/llm-client.js +237 -40
- package/dist/core/llm/llm-client.js.map +1 -1
- package/dist/core/s3-auto-updater.js +3 -3
- package/dist/core/s3-auto-updater.js.map +1 -1
- package/dist/core/session/session-manager.d.ts +0 -1
- package/dist/core/session/session-manager.d.ts.map +1 -1
- package/dist/core/session/session-manager.js +5 -3
- package/dist/core/session/session-manager.js.map +1 -1
- package/dist/core/slash-command-handler.js +1 -1
- package/dist/core/slash-command-handler.js.map +1 -1
- package/dist/core/telemetry/error-reporter.d.ts +2 -0
- package/dist/core/telemetry/error-reporter.d.ts.map +1 -0
- package/dist/core/telemetry/error-reporter.js +112 -0
- package/dist/core/telemetry/error-reporter.js.map +1 -0
- package/dist/core/usage-tracker.js +1 -1
- package/dist/core/usage-tracker.js.map +1 -1
- package/dist/orchestration/plan-executor.d.ts +0 -1
- package/dist/orchestration/plan-executor.d.ts.map +1 -1
- package/dist/orchestration/plan-executor.js +172 -57
- package/dist/orchestration/plan-executor.js.map +1 -1
- package/dist/prompts/agents/planning.d.ts.map +1 -1
- package/dist/prompts/agents/planning.js +25 -0
- package/dist/prompts/agents/planning.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -3
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +1 -3
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/shared/tool-usage.d.ts +2 -0
- package/dist/prompts/shared/tool-usage.d.ts.map +1 -1
- package/dist/prompts/shared/tool-usage.js +35 -1
- package/dist/prompts/shared/tool-usage.js.map +1 -1
- package/dist/prompts/system/plan-execute.d.ts +3 -0
- package/dist/prompts/system/plan-execute.d.ts.map +1 -1
- package/dist/prompts/system/plan-execute.js +49 -19
- package/dist/prompts/system/plan-execute.js.map +1 -1
- package/dist/tools/browser/browser-client.d.ts +0 -2
- package/dist/tools/browser/browser-client.d.ts.map +1 -1
- package/dist/tools/browser/browser-client.js +6 -12
- package/dist/tools/browser/browser-client.js.map +1 -1
- package/dist/tools/browser/browser-tools.js +2 -2
- package/dist/tools/browser/browser-tools.js.map +1 -1
- package/dist/tools/llm/index.d.ts +0 -1
- package/dist/tools/llm/index.d.ts.map +1 -1
- package/dist/tools/llm/index.js +0 -1
- package/dist/tools/llm/index.js.map +1 -1
- package/dist/tools/llm/simple/bash-tool.js +1 -1
- package/dist/tools/llm/simple/bash-tool.js.map +1 -1
- package/dist/tools/llm/simple/external-services/external-service-api-clients.d.ts +54 -0
- package/dist/tools/llm/simple/external-services/external-service-api-clients.d.ts.map +1 -0
- package/dist/tools/llm/simple/external-services/external-service-api-clients.js +208 -0
- package/dist/tools/llm/simple/external-services/external-service-api-clients.js.map +1 -0
- package/dist/tools/llm/simple/external-services/free-tools.d.ts +2 -0
- package/dist/tools/llm/simple/external-services/free-tools.d.ts.map +1 -0
- package/dist/tools/llm/simple/external-services/free-tools.js +2 -0
- package/dist/tools/llm/simple/external-services/free-tools.js.map +1 -0
- package/dist/tools/llm/simple/external-services/index.d.ts +3 -0
- package/dist/tools/llm/simple/external-services/index.d.ts.map +1 -0
- package/dist/tools/llm/simple/external-services/index.js +3 -0
- package/dist/tools/llm/simple/external-services/index.js.map +1 -0
- package/dist/tools/llm/simple/external-services/once-tools.d.ts +3 -0
- package/dist/tools/llm/simple/external-services/once-tools.d.ts.map +1 -0
- package/dist/tools/llm/simple/external-services/once-tools.js +106 -0
- package/dist/tools/llm/simple/external-services/once-tools.js.map +1 -0
- package/dist/tools/llm/simple/file-tools.d.ts +1 -1
- package/dist/tools/llm/simple/file-tools.d.ts.map +1 -1
- package/dist/tools/llm/simple/file-tools.js +49 -6
- package/dist/tools/llm/simple/file-tools.js.map +1 -1
- package/dist/tools/llm/simple/index.d.ts +1 -0
- package/dist/tools/llm/simple/index.d.ts.map +1 -1
- package/dist/tools/llm/simple/index.js +1 -0
- package/dist/tools/llm/simple/index.js.map +1 -1
- package/dist/tools/llm/simple/powershell-tool.d.ts.map +1 -1
- package/dist/tools/llm/simple/powershell-tool.js +16 -3
- package/dist/tools/llm/simple/powershell-tool.js.map +1 -1
- package/dist/tools/llm/simple/simple-tool-executor.d.ts +3 -0
- package/dist/tools/llm/simple/simple-tool-executor.d.ts.map +1 -1
- package/dist/tools/llm/simple/simple-tool-executor.js +9 -0
- package/dist/tools/llm/simple/simple-tool-executor.js.map +1 -1
- package/dist/tools/llm/simple/todo-tools.js +1 -1
- package/dist/tools/llm/simple/todo-tools.js.map +1 -1
- package/dist/tools/llm/simple/user-interaction-tools.js +1 -1
- package/dist/tools/llm/simple/user-interaction-tools.js.map +1 -1
- package/dist/tools/office/common/constants.d.ts +0 -2
- package/dist/tools/office/common/constants.d.ts.map +1 -1
- package/dist/tools/office/common/constants.js +0 -4
- package/dist/tools/office/common/constants.js.map +1 -1
- package/dist/tools/office/common/index.d.ts +1 -1
- package/dist/tools/office/common/index.d.ts.map +1 -1
- package/dist/tools/office/common/index.js +1 -1
- package/dist/tools/office/common/index.js.map +1 -1
- package/dist/tools/office/common/utils.d.ts.map +1 -1
- package/dist/tools/office/common/utils.js +2 -4
- package/dist/tools/office/common/utils.js.map +1 -1
- package/dist/tools/office/excel-client.d.ts +1 -0
- package/dist/tools/office/excel-client.d.ts.map +1 -1
- package/dist/tools/office/excel-client.js +7 -4
- package/dist/tools/office/excel-client.js.map +1 -1
- package/dist/tools/office/excel-tools/cells.d.ts.map +1 -1
- package/dist/tools/office/excel-tools/cells.js +5 -1
- package/dist/tools/office/excel-tools/cells.js.map +1 -1
- package/dist/tools/office/excel-tools/launch.js +3 -3
- package/dist/tools/office/excel-tools/launch.js.map +1 -1
- package/dist/tools/office/excel-tools/sheets.d.ts.map +1 -1
- package/dist/tools/office/excel-tools/sheets.js +6 -1
- package/dist/tools/office/excel-tools/sheets.js.map +1 -1
- package/dist/tools/office/excel-tools/tables.d.ts.map +1 -1
- package/dist/tools/office/excel-tools/tables.js +6 -1
- package/dist/tools/office/excel-tools/tables.js.map +1 -1
- package/dist/tools/office/office-client-base.d.ts +1 -0
- package/dist/tools/office/office-client-base.d.ts.map +1 -1
- package/dist/tools/office/office-client-base.js +12 -1
- package/dist/tools/office/office-client-base.js.map +1 -1
- package/dist/tools/office/powerpoint-client.d.ts +1 -0
- package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-client.js +9 -1
- package/dist/tools/office/powerpoint-client.js.map +1 -1
- package/dist/tools/office/powerpoint-tools/launch.js +3 -3
- package/dist/tools/office/powerpoint-tools/launch.js.map +1 -1
- package/dist/tools/office/word-client.d.ts +1 -0
- package/dist/tools/office/word-client.d.ts.map +1 -1
- package/dist/tools/office/word-client.js +7 -2
- package/dist/tools/office/word-client.js.map +1 -1
- package/dist/tools/office/word-tools/launch.js +3 -3
- package/dist/tools/office/word-tools/launch.js.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +8 -5
- package/dist/tools/registry.js.map +1 -1
- package/dist/ui/TodoPanel.d.ts.map +1 -1
- package/dist/ui/TodoPanel.js +7 -4
- package/dist/ui/TodoPanel.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +5 -4
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/CommandBrowser.d.ts.map +1 -1
- package/dist/ui/components/CommandBrowser.js +7 -3
- package/dist/ui/components/CommandBrowser.js.map +1 -1
- package/dist/ui/components/CustomTextInput.d.ts.map +1 -1
- package/dist/ui/components/CustomTextInput.js +2 -1
- package/dist/ui/components/CustomTextInput.js.map +1 -1
- package/dist/ui/components/PlanExecuteApp.d.ts +1 -1
- package/dist/ui/components/PlanExecuteApp.d.ts.map +1 -1
- package/dist/ui/components/PlanExecuteApp.js +41 -87
- package/dist/ui/components/PlanExecuteApp.js.map +1 -1
- package/dist/ui/components/dialogs/ApprovalDialog.d.ts.map +1 -1
- package/dist/ui/components/dialogs/ApprovalDialog.js +5 -3
- package/dist/ui/components/dialogs/ApprovalDialog.js.map +1 -1
- package/dist/ui/components/dialogs/DocsBrowser.js +2 -2
- package/dist/ui/components/dialogs/DocsBrowser.js.map +1 -1
- package/dist/ui/hooks/index.d.ts +1 -0
- package/dist/ui/hooks/index.d.ts.map +1 -1
- package/dist/ui/hooks/index.js +1 -0
- package/dist/ui/hooks/index.js.map +1 -1
- package/dist/ui/hooks/useFileBrowserState.js +1 -1
- package/dist/ui/hooks/useFileBrowserState.js.map +1 -1
- package/dist/ui/hooks/useFileList.js +2 -2
- package/dist/ui/hooks/useFileList.js.map +1 -1
- package/dist/ui/hooks/useTerminalWidth.d.ts +4 -0
- package/dist/ui/hooks/useTerminalWidth.d.ts.map +1 -0
- package/dist/ui/hooks/useTerminalWidth.js +26 -0
- package/dist/ui/hooks/useTerminalWidth.js.map +1 -0
- package/dist/ui/ink-entry.js +1 -1
- package/dist/ui/ink-entry.js.map +1 -1
- package/dist/utils/json-stream-logger.js +0 -2
- package/dist/utils/json-stream-logger.js.map +1 -1
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +18 -0
- package/dist/utils/logger.js.map +1 -1
- package/package.json +3 -1
|
@@ -4,6 +4,7 @@ import { NetworkError, APIError, TimeoutError, ConnectionError, } from '../../er
|
|
|
4
4
|
import { LLMError, TokenLimitError, RateLimitError, ContextLengthError, } from '../../errors/llm.js';
|
|
5
5
|
import { QuotaExceededError } from '../../errors/quota.js';
|
|
6
6
|
import { logger, isLLMLogEnabled } from '../../utils/logger.js';
|
|
7
|
+
import { reportError } from '../telemetry/error-reporter.js';
|
|
7
8
|
import { usageTracker } from '../usage-tracker.js';
|
|
8
9
|
export class LLMClient {
|
|
9
10
|
axiosInstance;
|
|
@@ -35,14 +36,33 @@ export class LLMClient {
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
preprocessMessages(messages, modelId) {
|
|
38
|
-
|
|
39
|
+
let lastAssistantIdx = -1;
|
|
40
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
41
|
+
if (messages[i].role === 'assistant') {
|
|
42
|
+
lastAssistantIdx = i;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return messages.map((msg, index) => {
|
|
39
47
|
if (msg.role !== 'assistant') {
|
|
40
48
|
return msg;
|
|
41
49
|
}
|
|
42
50
|
const msgAny = msg;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
const processedMsg = { ...msg };
|
|
52
|
+
const isLatestAssistant = index === lastAssistantIdx;
|
|
53
|
+
if (!isLatestAssistant) {
|
|
54
|
+
if (msgAny.reasoning_content) {
|
|
55
|
+
delete processedMsg.reasoning_content;
|
|
56
|
+
}
|
|
57
|
+
if (msgAny.reasoning) {
|
|
58
|
+
delete processedMsg.reasoning;
|
|
59
|
+
}
|
|
60
|
+
if (processedMsg.content) {
|
|
61
|
+
processedMsg.content = processedMsg.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (msgAny.reasoning_content && (!processedMsg.content || processedMsg.content.trim() === '')) {
|
|
65
|
+
processedMsg.content = isLatestAssistant ? msgAny.reasoning_content : '';
|
|
46
66
|
delete processedMsg.reasoning_content;
|
|
47
67
|
}
|
|
48
68
|
if (/^gpt-oss-(120b|20b)$/i.test(modelId)) {
|
|
@@ -109,7 +129,7 @@ export class LLMClient {
|
|
|
109
129
|
const elapsed = logger.endTimer('llm-api-call');
|
|
110
130
|
logger.flow('API 응답 수신 완료');
|
|
111
131
|
if (!response.data.choices || !Array.isArray(response.data.choices)) {
|
|
112
|
-
logger.
|
|
132
|
+
logger.errorSilent('Invalid response structure - missing choices array', response.data);
|
|
113
133
|
throw new Error('LLM 응답 형식이 올바르지 않습니다. choices 배열이 없습니다.');
|
|
114
134
|
}
|
|
115
135
|
logger.httpResponse(response.status, response.statusText, {
|
|
@@ -168,17 +188,19 @@ export class LLMClient {
|
|
|
168
188
|
}
|
|
169
189
|
logger.flow('API 호출 실패 - 에러 처리');
|
|
170
190
|
if (currentAttempt > 1) {
|
|
171
|
-
logger.
|
|
191
|
+
logger.errorSilent(`LLM 호출 ${maxRetries}번 재시도 후 최종 실패`, {
|
|
172
192
|
error: error.message,
|
|
173
193
|
attempts: currentAttempt
|
|
174
194
|
});
|
|
175
195
|
}
|
|
176
196
|
logger.exit('chatCompletion', { success: false, error: error.message, attempts: currentAttempt });
|
|
177
|
-
|
|
197
|
+
const handled = this.handleError(error, {
|
|
178
198
|
method: 'POST',
|
|
179
199
|
url,
|
|
180
200
|
body: options,
|
|
181
201
|
});
|
|
202
|
+
reportError(handled, { type: 'llm', method: 'chatCompletion', endpoint: url, modelId: this.model, modelName: this.modelName }).catch(() => { });
|
|
203
|
+
throw handled;
|
|
182
204
|
}
|
|
183
205
|
}
|
|
184
206
|
abort() {
|
|
@@ -309,11 +331,13 @@ export class LLMClient {
|
|
|
309
331
|
logger.debug('Streaming response completed', { chunkCount });
|
|
310
332
|
}
|
|
311
333
|
catch (error) {
|
|
312
|
-
|
|
334
|
+
const handled = this.handleError(error, {
|
|
313
335
|
method: 'POST (stream)',
|
|
314
336
|
url,
|
|
315
337
|
body: options,
|
|
316
338
|
});
|
|
339
|
+
reportError(handled, { type: 'llm', method: 'chatCompletionStream', endpoint: url, modelId: this.model, modelName: this.modelName }).catch(() => { });
|
|
340
|
+
throw handled;
|
|
317
341
|
}
|
|
318
342
|
}
|
|
319
343
|
async sendMessage(userMessage, systemPrompt) {
|
|
@@ -375,24 +399,60 @@ export class LLMClient {
|
|
|
375
399
|
}
|
|
376
400
|
async chatCompletionWithTools(messages, tools, options) {
|
|
377
401
|
let workingMessages = [...messages];
|
|
402
|
+
const toolLoopMessages = [];
|
|
378
403
|
const toolCallHistory = [];
|
|
379
404
|
let iterations = 0;
|
|
380
405
|
let contextLengthRecoveryAttempted = false;
|
|
381
406
|
let noToolCallRetries = 0;
|
|
382
407
|
let finalResponseFailures = 0;
|
|
408
|
+
let consecutiveParseFailures = 0;
|
|
383
409
|
const MAX_NO_TOOL_CALL_RETRIES = 3;
|
|
384
410
|
const MAX_FINAL_RESPONSE_FAILURES = 3;
|
|
411
|
+
const MAX_CONSECUTIVE_PARSE_FAILURES = 3;
|
|
412
|
+
const parseFailureToolCallIds = new Set();
|
|
413
|
+
const stripParseFailures = (msgs) => {
|
|
414
|
+
if (parseFailureToolCallIds.size === 0)
|
|
415
|
+
return msgs;
|
|
416
|
+
return msgs.filter(msg => {
|
|
417
|
+
if (msg.role === 'tool' && msg.tool_call_id && parseFailureToolCallIds.has(msg.tool_call_id))
|
|
418
|
+
return false;
|
|
419
|
+
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0 &&
|
|
420
|
+
msg.tool_calls.every(tc => parseFailureToolCallIds.has(tc.id)))
|
|
421
|
+
return false;
|
|
422
|
+
return true;
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
const addMessage = (msg) => {
|
|
426
|
+
workingMessages.push(msg);
|
|
427
|
+
if (options?.rebuildMessages) {
|
|
428
|
+
toolLoopMessages.push(msg);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const getAllMessages = () => {
|
|
432
|
+
return options?.rebuildMessages
|
|
433
|
+
? stripParseFailures(toolLoopMessages)
|
|
434
|
+
: stripParseFailures(workingMessages);
|
|
435
|
+
};
|
|
385
436
|
while (true) {
|
|
386
437
|
if (this.isInterrupted) {
|
|
387
438
|
logger.flow('Interrupt detected - stopping tool loop');
|
|
388
439
|
throw new Error('INTERRUPTED');
|
|
389
440
|
}
|
|
390
441
|
iterations++;
|
|
442
|
+
if (options?.rebuildMessages) {
|
|
443
|
+
workingMessages = options.rebuildMessages(toolLoopMessages);
|
|
444
|
+
}
|
|
391
445
|
if (options?.getPendingMessage && options?.clearPendingMessage) {
|
|
392
446
|
const pendingMsg = options.getPendingMessage();
|
|
393
447
|
if (pendingMsg) {
|
|
394
448
|
logger.flow('Injecting pending user message into conversation');
|
|
395
|
-
|
|
449
|
+
if (options?.rebuildMessages) {
|
|
450
|
+
toolLoopMessages.push({ role: 'user', content: pendingMsg });
|
|
451
|
+
workingMessages = options.rebuildMessages(toolLoopMessages);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
workingMessages.push({ role: 'user', content: pendingMsg });
|
|
455
|
+
}
|
|
396
456
|
options.clearPendingMessage();
|
|
397
457
|
}
|
|
398
458
|
}
|
|
@@ -407,7 +467,19 @@ export class LLMClient {
|
|
|
407
467
|
catch (error) {
|
|
408
468
|
if (error instanceof ContextLengthError && !contextLengthRecoveryAttempted) {
|
|
409
469
|
contextLengthRecoveryAttempted = true;
|
|
410
|
-
logger.flow('ContextLengthError detected - attempting recovery
|
|
470
|
+
logger.flow('ContextLengthError detected - attempting recovery');
|
|
471
|
+
if (options?.rebuildMessages) {
|
|
472
|
+
let rollbackIdx = toolLoopMessages.length - 1;
|
|
473
|
+
while (rollbackIdx >= 0 && toolLoopMessages[rollbackIdx]?.role === 'tool') {
|
|
474
|
+
rollbackIdx--;
|
|
475
|
+
}
|
|
476
|
+
if (rollbackIdx >= 0 && toolLoopMessages[rollbackIdx]?.tool_calls) {
|
|
477
|
+
toolLoopMessages.length = rollbackIdx;
|
|
478
|
+
logger.debug('Rolled back toolLoopMessages', { newLength: toolLoopMessages.length });
|
|
479
|
+
}
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
logger.flow('Attempting recovery with compact');
|
|
411
483
|
let rollbackIdx = workingMessages.length - 1;
|
|
412
484
|
while (rollbackIdx >= 0 && workingMessages[rollbackIdx]?.role === 'tool') {
|
|
413
485
|
rollbackIdx--;
|
|
@@ -436,7 +508,7 @@ export class LLMClient {
|
|
|
436
508
|
continue;
|
|
437
509
|
}
|
|
438
510
|
else {
|
|
439
|
-
logger.
|
|
511
|
+
logger.errorSilent('Compact failed during recovery', { error: compactResult.error });
|
|
440
512
|
throw error;
|
|
441
513
|
}
|
|
442
514
|
}
|
|
@@ -451,7 +523,7 @@ export class LLMClient {
|
|
|
451
523
|
throw new Error('응답에서 choice를 찾을 수 없습니다.');
|
|
452
524
|
}
|
|
453
525
|
const assistantMessage = choice.message;
|
|
454
|
-
|
|
526
|
+
addMessage(assistantMessage);
|
|
455
527
|
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
|
|
456
528
|
if (assistantMessage.tool_calls.length > 1) {
|
|
457
529
|
const toolNames = assistantMessage.tool_calls.map(tc => tc.function.name).join(', ');
|
|
@@ -459,18 +531,86 @@ export class LLMClient {
|
|
|
459
531
|
assistantMessage.tool_calls = [assistantMessage.tool_calls[0]];
|
|
460
532
|
}
|
|
461
533
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
462
|
-
const
|
|
534
|
+
const rawToolName = toolCall.function.name;
|
|
535
|
+
const toolName = rawToolName.replace(/<\|.*$/, '').replace(/[^a-zA-Z0-9_-]+$/, '').trim() || rawToolName;
|
|
536
|
+
if (toolName !== rawToolName) {
|
|
537
|
+
logger.warn('Tool name sanitized (model leaked special tokens)', {
|
|
538
|
+
original: rawToolName,
|
|
539
|
+
sanitized: toolName,
|
|
540
|
+
model: this.model,
|
|
541
|
+
});
|
|
542
|
+
toolCall.function.name = toolName;
|
|
543
|
+
reportError(new Error(`Tool name contaminated: ${rawToolName}`), {
|
|
544
|
+
type: 'toolNameContamination',
|
|
545
|
+
original: rawToolName,
|
|
546
|
+
sanitized: toolName,
|
|
547
|
+
modelId: this.model,
|
|
548
|
+
modelName: this.modelName,
|
|
549
|
+
}).catch(() => { });
|
|
550
|
+
}
|
|
463
551
|
let toolArgs;
|
|
464
552
|
try {
|
|
465
553
|
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
466
554
|
}
|
|
467
555
|
catch (parseError) {
|
|
468
|
-
|
|
469
|
-
|
|
556
|
+
consecutiveParseFailures++;
|
|
557
|
+
parseFailureToolCallIds.add(toolCall.id);
|
|
558
|
+
const errorMsg = `Tool argument parsing failed for ${toolName}: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`;
|
|
559
|
+
logger.errorSilent('Tool argument parse error', {
|
|
560
|
+
toolName,
|
|
561
|
+
error: errorMsg,
|
|
562
|
+
consecutiveFailures: consecutiveParseFailures,
|
|
563
|
+
});
|
|
470
564
|
logger.debug('Raw arguments', { raw: toolCall.function.arguments });
|
|
471
|
-
|
|
565
|
+
reportError(parseError, {
|
|
566
|
+
type: 'toolArgParsing',
|
|
567
|
+
tool: toolName,
|
|
568
|
+
consecutiveFailures: consecutiveParseFailures,
|
|
569
|
+
modelId: this.model,
|
|
570
|
+
modelName: this.modelName,
|
|
571
|
+
rawArguments: typeof toolCall.function.arguments === 'string' ? toolCall.function.arguments.substring(0, 500) : undefined,
|
|
572
|
+
}).catch(() => { });
|
|
573
|
+
if (consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
|
|
574
|
+
logger.errorSilent('[ABORT] Tool argument parse failed 3 times consecutively. Model may not support JSON function calling.');
|
|
575
|
+
const abortMsg = 'I cannot generate valid JSON tool arguments. Please try a different model that supports JSON function calling.';
|
|
576
|
+
addMessage({
|
|
577
|
+
role: 'tool',
|
|
578
|
+
content: errorMsg,
|
|
579
|
+
tool_call_id: toolCall.id,
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
message: { role: 'assistant', content: abortMsg },
|
|
583
|
+
toolCalls: toolCallHistory,
|
|
584
|
+
allMessages: getAllMessages(),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const rawArgs = toolCall.function.arguments;
|
|
588
|
+
const rawPreview = typeof rawArgs === 'string' ? rawArgs.substring(0, 300) : String(rawArgs);
|
|
589
|
+
const hintMsg = `Error: Failed to parse tool arguments for "${toolName}".
|
|
590
|
+
|
|
591
|
+
Parse error: ${parseError instanceof Error ? parseError.message : 'Unknown error'}
|
|
592
|
+
|
|
593
|
+
Your raw input was:
|
|
594
|
+
\`\`\`
|
|
595
|
+
${rawPreview}
|
|
596
|
+
\`\`\`
|
|
597
|
+
|
|
598
|
+
Fix the following issues:
|
|
599
|
+
1. Arguments MUST be valid JSON (not XML, not plain text)
|
|
600
|
+
2. All strings must use double quotes ("), not single quotes (')
|
|
601
|
+
3. No trailing commas after the last property
|
|
602
|
+
4. No comments inside JSON
|
|
603
|
+
5. Escape special characters in strings (\\n, \\", \\\\)
|
|
604
|
+
|
|
605
|
+
Correct format example:
|
|
606
|
+
\`\`\`json
|
|
607
|
+
{"reason": "description", "file_path": "src/index.ts"}
|
|
608
|
+
\`\`\`
|
|
609
|
+
|
|
610
|
+
Do NOT use XML tags like <arg_key> or <arg_value>. Retry with valid JSON.`;
|
|
611
|
+
addMessage({
|
|
472
612
|
role: 'tool',
|
|
473
|
-
content:
|
|
613
|
+
content: hintMsg,
|
|
474
614
|
tool_call_id: toolCall.id,
|
|
475
615
|
});
|
|
476
616
|
toolCallHistory.push({
|
|
@@ -480,6 +620,59 @@ export class LLMClient {
|
|
|
480
620
|
});
|
|
481
621
|
continue;
|
|
482
622
|
}
|
|
623
|
+
const toolDef = tools.find(t => t.function.name === toolName);
|
|
624
|
+
if (toolDef?.function.parameters) {
|
|
625
|
+
const schema = toolDef.function.parameters;
|
|
626
|
+
const schemaErrors = [];
|
|
627
|
+
if (schema.required) {
|
|
628
|
+
for (const req of schema.required) {
|
|
629
|
+
if (toolArgs[req] === undefined || toolArgs[req] === null) {
|
|
630
|
+
const propDef = schema.properties[req];
|
|
631
|
+
schemaErrors.push(`Missing required parameter: "${req}" (expected: ${propDef?.type || 'unknown'})`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (const [key, value] of Object.entries(toolArgs)) {
|
|
636
|
+
const propDef = schema.properties[key];
|
|
637
|
+
if (propDef?.type && value !== null && value !== undefined) {
|
|
638
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
639
|
+
if (actualType !== propDef.type) {
|
|
640
|
+
schemaErrors.push(`"${key}": expected ${propDef.type}, got ${actualType} (${JSON.stringify(value).substring(0, 50)})`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (schemaErrors.length > 0) {
|
|
645
|
+
consecutiveParseFailures++;
|
|
646
|
+
parseFailureToolCallIds.add(toolCall.id);
|
|
647
|
+
if (consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
|
|
648
|
+
const abortMsg = 'Cannot generate valid tool arguments. Please try a different model.';
|
|
649
|
+
addMessage({ role: 'tool', content: schemaErrors.join('\n'), tool_call_id: toolCall.id });
|
|
650
|
+
return {
|
|
651
|
+
message: { role: 'assistant', content: abortMsg },
|
|
652
|
+
toolCalls: toolCallHistory,
|
|
653
|
+
allMessages: getAllMessages(),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const requiredList = (schema.required || [])
|
|
657
|
+
.map(r => {
|
|
658
|
+
const p = schema.properties[r];
|
|
659
|
+
return ` "${r}": ${p?.type || 'unknown'}`;
|
|
660
|
+
})
|
|
661
|
+
.join('\n');
|
|
662
|
+
const hintMsg = `Error: Schema validation failed for "${toolName}".
|
|
663
|
+
|
|
664
|
+
${schemaErrors.join('\n')}
|
|
665
|
+
|
|
666
|
+
Required parameters:
|
|
667
|
+
${requiredList}
|
|
668
|
+
|
|
669
|
+
Retry with correct parameter names and types.`;
|
|
670
|
+
addMessage({ role: 'tool', content: hintMsg, tool_call_id: toolCall.id });
|
|
671
|
+
toolCallHistory.push({ tool: toolName, args: toolArgs, result: 'Error: Schema validation failed' });
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
consecutiveParseFailures = 0;
|
|
483
676
|
const { executeFileTool, requestToolApproval } = await import('../../tools/llm/simple/file-tools.js');
|
|
484
677
|
const approvalResult = await requestToolApproval(toolName, toolArgs);
|
|
485
678
|
if (approvalResult && typeof approvalResult === 'object' && approvalResult.reject) {
|
|
@@ -487,7 +680,7 @@ export class LLMClient {
|
|
|
487
680
|
const rejectMessage = approvalResult.comment
|
|
488
681
|
? `Tool execution rejected by user. Reason: ${approvalResult.comment}`
|
|
489
682
|
: 'Tool execution rejected by user.';
|
|
490
|
-
|
|
683
|
+
addMessage({
|
|
491
684
|
role: 'tool',
|
|
492
685
|
content: rejectMessage,
|
|
493
686
|
tool_call_id: toolCall.id,
|
|
@@ -510,7 +703,7 @@ export class LLMClient {
|
|
|
510
703
|
if (toolName === 'final_response') {
|
|
511
704
|
if (result.success && result.metadata?.['isFinalResponse']) {
|
|
512
705
|
logger.flow('final_response tool executed successfully - returning');
|
|
513
|
-
|
|
706
|
+
addMessage({
|
|
514
707
|
role: 'tool',
|
|
515
708
|
content: result.result || '',
|
|
516
709
|
tool_call_id: toolCall.id,
|
|
@@ -526,7 +719,7 @@ export class LLMClient {
|
|
|
526
719
|
content: result.result || '',
|
|
527
720
|
},
|
|
528
721
|
toolCalls: toolCallHistory,
|
|
529
|
-
allMessages:
|
|
722
|
+
allMessages: getAllMessages(),
|
|
530
723
|
};
|
|
531
724
|
}
|
|
532
725
|
else {
|
|
@@ -540,7 +733,7 @@ export class LLMClient {
|
|
|
540
733
|
return {
|
|
541
734
|
message: { role: 'assistant', content: fallbackMessage },
|
|
542
735
|
toolCalls: toolCallHistory,
|
|
543
|
-
allMessages:
|
|
736
|
+
allMessages: getAllMessages(),
|
|
544
737
|
};
|
|
545
738
|
}
|
|
546
739
|
}
|
|
@@ -548,6 +741,7 @@ export class LLMClient {
|
|
|
548
741
|
}
|
|
549
742
|
catch (toolError) {
|
|
550
743
|
logger.toolExecution(toolName, toolArgs, undefined, toolError);
|
|
744
|
+
reportError(toolError, { type: 'toolExecution', tool: toolName, modelId: this.model, modelName: this.modelName, toolArgs }).catch(() => { });
|
|
551
745
|
if (isLLMLogEnabled()) {
|
|
552
746
|
const errorMsg = toolError instanceof Error ? toolError.message : String(toolError);
|
|
553
747
|
logger.llmToolResult(toolName, `Error: ${errorMsg}`, false);
|
|
@@ -557,7 +751,7 @@ export class LLMClient {
|
|
|
557
751
|
error: toolError instanceof Error ? toolError.message : String(toolError),
|
|
558
752
|
};
|
|
559
753
|
}
|
|
560
|
-
|
|
754
|
+
addMessage({
|
|
561
755
|
role: 'tool',
|
|
562
756
|
content: result.success ? result.result || '' : `Error: ${result.error}`,
|
|
563
757
|
tool_call_id: toolCall.id,
|
|
@@ -572,6 +766,9 @@ export class LLMClient {
|
|
|
572
766
|
throw new Error('INTERRUPTED');
|
|
573
767
|
}
|
|
574
768
|
}
|
|
769
|
+
if (options?.onAfterToolExecution) {
|
|
770
|
+
await options.onAfterToolExecution(toolLoopMessages);
|
|
771
|
+
}
|
|
575
772
|
continue;
|
|
576
773
|
}
|
|
577
774
|
else {
|
|
@@ -585,7 +782,7 @@ export class LLMClient {
|
|
|
585
782
|
return {
|
|
586
783
|
message: { role: 'assistant', content: fallbackContent },
|
|
587
784
|
toolCalls: toolCallHistory,
|
|
588
|
-
allMessages:
|
|
785
|
+
allMessages: getAllMessages(),
|
|
589
786
|
};
|
|
590
787
|
}
|
|
591
788
|
const hasMalformedToolCall = assistantMessage.content &&
|
|
@@ -597,7 +794,7 @@ export class LLMClient {
|
|
|
597
794
|
const retryMessage = hasMalformedToolCall
|
|
598
795
|
? 'Your previous response contained a malformed tool call (XML tags in content). You MUST use the proper tool_calls API format. Use final_response tool to deliver your message to the user.'
|
|
599
796
|
: 'You must use tools for all actions. Use final_response tool to deliver your final message to the user after completing all tasks.';
|
|
600
|
-
|
|
797
|
+
addMessage({
|
|
601
798
|
role: 'user',
|
|
602
799
|
content: retryMessage,
|
|
603
800
|
});
|
|
@@ -613,14 +810,14 @@ export class LLMClient {
|
|
|
613
810
|
};
|
|
614
811
|
}
|
|
615
812
|
handleError(error, requestContext) {
|
|
616
|
-
logger.
|
|
813
|
+
logger.errorSilent('LLM Client Error', error);
|
|
617
814
|
if (requestContext) {
|
|
618
815
|
logger.debug('Request Context', requestContext);
|
|
619
816
|
}
|
|
620
817
|
if (axios.isAxiosError(error)) {
|
|
621
818
|
const axiosError = error;
|
|
622
819
|
if (axiosError.code === 'ECONNABORTED' || axiosError.message.includes('timeout')) {
|
|
623
|
-
logger.
|
|
820
|
+
logger.errorSilent('Request Timeout', {
|
|
624
821
|
timeout: this.axiosInstance.defaults.timeout,
|
|
625
822
|
endpoint: this.baseUrl,
|
|
626
823
|
});
|
|
@@ -639,7 +836,7 @@ export class LLMClient {
|
|
|
639
836
|
const errorMessage = data?.error?.message || data?.message || axiosError.message;
|
|
640
837
|
const errorType = data?.error?.type || 'unknown';
|
|
641
838
|
const errorCode = data?.error?.code || data?.code;
|
|
642
|
-
logger.
|
|
839
|
+
logger.errorSilent('=== API ERROR DETAILS ===', {
|
|
643
840
|
status,
|
|
644
841
|
statusText: axiosError.response.statusText,
|
|
645
842
|
endpoint: this.baseUrl,
|
|
@@ -661,7 +858,7 @@ export class LLMClient {
|
|
|
661
858
|
errorMessage.includes('maximum context length') ||
|
|
662
859
|
errorCode === 'context_length_exceeded')) {
|
|
663
860
|
const maxLength = data?.error?.param?.max_tokens || 'unknown';
|
|
664
|
-
logger.
|
|
861
|
+
logger.errorSilent('Context Length Exceeded', {
|
|
665
862
|
maxLength,
|
|
666
863
|
errorMessage,
|
|
667
864
|
model: this.model,
|
|
@@ -678,7 +875,7 @@ export class LLMClient {
|
|
|
678
875
|
}
|
|
679
876
|
if (errorMessage.includes('token') &&
|
|
680
877
|
(errorMessage.includes('limit') || errorMessage.includes('exceeded'))) {
|
|
681
|
-
logger.
|
|
878
|
+
logger.errorSilent('Token Limit Error', {
|
|
682
879
|
errorMessage,
|
|
683
880
|
model: this.model,
|
|
684
881
|
});
|
|
@@ -694,7 +891,7 @@ export class LLMClient {
|
|
|
694
891
|
}
|
|
695
892
|
if (status === 429) {
|
|
696
893
|
if (errorType === 'quota_exceeded' && data?.error?.quota) {
|
|
697
|
-
logger.
|
|
894
|
+
logger.errorSilent('Quota Exceeded', {
|
|
698
895
|
period: data.error.quota.period,
|
|
699
896
|
weekly: data.error.quota.weekly,
|
|
700
897
|
});
|
|
@@ -702,7 +899,7 @@ export class LLMClient {
|
|
|
702
899
|
}
|
|
703
900
|
const retryAfter = axiosError.response.headers['retry-after'];
|
|
704
901
|
const retrySeconds = retryAfter ? parseInt(retryAfter) : undefined;
|
|
705
|
-
logger.
|
|
902
|
+
logger.errorSilent('Rate Limit Exceeded', {
|
|
706
903
|
retryAfter: retrySeconds,
|
|
707
904
|
errorMessage,
|
|
708
905
|
});
|
|
@@ -716,7 +913,7 @@ export class LLMClient {
|
|
|
716
913
|
});
|
|
717
914
|
}
|
|
718
915
|
if (status === 401) {
|
|
719
|
-
logger.
|
|
916
|
+
logger.errorSilent('Authentication Failed', {
|
|
720
917
|
endpoint: this.baseUrl,
|
|
721
918
|
errorMessage,
|
|
722
919
|
});
|
|
@@ -732,7 +929,7 @@ export class LLMClient {
|
|
|
732
929
|
});
|
|
733
930
|
}
|
|
734
931
|
if (status === 403) {
|
|
735
|
-
logger.
|
|
932
|
+
logger.errorSilent('Access Forbidden', {
|
|
736
933
|
endpoint: this.baseUrl,
|
|
737
934
|
errorMessage,
|
|
738
935
|
});
|
|
@@ -745,7 +942,7 @@ export class LLMClient {
|
|
|
745
942
|
});
|
|
746
943
|
}
|
|
747
944
|
if (status === 404) {
|
|
748
|
-
logger.
|
|
945
|
+
logger.errorSilent('Endpoint Not Found', {
|
|
749
946
|
endpoint: this.baseUrl,
|
|
750
947
|
url: requestContext?.url,
|
|
751
948
|
errorMessage,
|
|
@@ -761,7 +958,7 @@ export class LLMClient {
|
|
|
761
958
|
});
|
|
762
959
|
}
|
|
763
960
|
if (status >= 500) {
|
|
764
|
-
logger.
|
|
961
|
+
logger.errorSilent('Server Error', {
|
|
765
962
|
status,
|
|
766
963
|
endpoint: this.baseUrl,
|
|
767
964
|
errorMessage,
|
|
@@ -774,7 +971,7 @@ export class LLMClient {
|
|
|
774
971
|
isRecoverable: true,
|
|
775
972
|
});
|
|
776
973
|
}
|
|
777
|
-
logger.
|
|
974
|
+
logger.errorSilent('API Error', {
|
|
778
975
|
status,
|
|
779
976
|
endpoint: this.baseUrl,
|
|
780
977
|
errorMessage,
|
|
@@ -793,7 +990,7 @@ export class LLMClient {
|
|
|
793
990
|
}
|
|
794
991
|
else if (axiosError.request) {
|
|
795
992
|
const errorCode = axiosError.code;
|
|
796
|
-
logger.
|
|
993
|
+
logger.errorSilent('Network Error - No Response', {
|
|
797
994
|
code: errorCode,
|
|
798
995
|
endpoint: this.baseUrl,
|
|
799
996
|
message: axiosError.message,
|
|
@@ -820,7 +1017,7 @@ export class LLMClient {
|
|
|
820
1017
|
userMessage: `네트워크 연결 실패.\n엔드포인트: ${this.baseUrl}\n에러: ${axiosError.message}`,
|
|
821
1018
|
});
|
|
822
1019
|
}
|
|
823
|
-
logger.
|
|
1020
|
+
logger.errorSilent('Axios Error', {
|
|
824
1021
|
code: axiosError.code,
|
|
825
1022
|
message: axiosError.message,
|
|
826
1023
|
});
|
|
@@ -832,13 +1029,13 @@ export class LLMClient {
|
|
|
832
1029
|
});
|
|
833
1030
|
}
|
|
834
1031
|
if (error instanceof Error) {
|
|
835
|
-
logger.
|
|
1032
|
+
logger.errorSilent('Unexpected Error', error);
|
|
836
1033
|
return new LLMError(`예상치 못한 에러: ${error.message}`, {
|
|
837
1034
|
cause: error,
|
|
838
1035
|
userMessage: `오류가 발생했습니다:\n${error.message}\n\n스택:\n${error.stack}`,
|
|
839
1036
|
});
|
|
840
1037
|
}
|
|
841
|
-
logger.
|
|
1038
|
+
logger.errorSilent('Unknown Error Type', { error });
|
|
842
1039
|
return new LLMError('알 수 없는 에러가 발생했습니다.', {
|
|
843
1040
|
details: { unknownError: error },
|
|
844
1041
|
});
|