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.
Files changed (200) hide show
  1. package/dist/agents/index.d.ts +1 -2
  2. package/dist/agents/index.d.ts.map +1 -1
  3. package/dist/agents/index.js +0 -1
  4. package/dist/agents/index.js.map +1 -1
  5. package/dist/agents/planner/index.d.ts +0 -4
  6. package/dist/agents/planner/index.d.ts.map +1 -1
  7. package/dist/agents/planner/index.js +19 -13
  8. package/dist/agents/planner/index.js.map +1 -1
  9. package/dist/cli.js +11 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/constants.d.ts +3 -1
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/constants.js +3 -1
  14. package/dist/constants.js.map +1 -1
  15. package/dist/core/auth/auth-gate.d.ts.map +1 -1
  16. package/dist/core/auth/auth-gate.js +3 -3
  17. package/dist/core/auth/auth-gate.js.map +1 -1
  18. package/dist/core/background-sync.d.ts +39 -0
  19. package/dist/core/background-sync.d.ts.map +1 -0
  20. package/dist/core/background-sync.js +249 -0
  21. package/dist/core/background-sync.js.map +1 -0
  22. package/dist/core/compact/compact-manager.d.ts.map +1 -1
  23. package/dist/core/compact/compact-manager.js +4 -2
  24. package/dist/core/compact/compact-manager.js.map +1 -1
  25. package/dist/core/config/config-manager.js +1 -1
  26. package/dist/core/config/config-manager.js.map +1 -1
  27. package/dist/core/docs-manager.js +3 -3
  28. package/dist/core/docs-manager.js.map +1 -1
  29. package/dist/core/llm/llm-client.d.ts +2 -0
  30. package/dist/core/llm/llm-client.d.ts.map +1 -1
  31. package/dist/core/llm/llm-client.js +237 -40
  32. package/dist/core/llm/llm-client.js.map +1 -1
  33. package/dist/core/s3-auto-updater.js +3 -3
  34. package/dist/core/s3-auto-updater.js.map +1 -1
  35. package/dist/core/session/session-manager.d.ts +0 -1
  36. package/dist/core/session/session-manager.d.ts.map +1 -1
  37. package/dist/core/session/session-manager.js +5 -3
  38. package/dist/core/session/session-manager.js.map +1 -1
  39. package/dist/core/slash-command-handler.js +1 -1
  40. package/dist/core/slash-command-handler.js.map +1 -1
  41. package/dist/core/telemetry/error-reporter.d.ts +2 -0
  42. package/dist/core/telemetry/error-reporter.d.ts.map +1 -0
  43. package/dist/core/telemetry/error-reporter.js +112 -0
  44. package/dist/core/telemetry/error-reporter.js.map +1 -0
  45. package/dist/core/usage-tracker.js +1 -1
  46. package/dist/core/usage-tracker.js.map +1 -1
  47. package/dist/orchestration/plan-executor.d.ts +0 -1
  48. package/dist/orchestration/plan-executor.d.ts.map +1 -1
  49. package/dist/orchestration/plan-executor.js +172 -57
  50. package/dist/orchestration/plan-executor.js.map +1 -1
  51. package/dist/prompts/agents/planning.d.ts.map +1 -1
  52. package/dist/prompts/agents/planning.js +25 -0
  53. package/dist/prompts/agents/planning.js.map +1 -1
  54. package/dist/prompts/index.d.ts +1 -3
  55. package/dist/prompts/index.d.ts.map +1 -1
  56. package/dist/prompts/index.js +1 -3
  57. package/dist/prompts/index.js.map +1 -1
  58. package/dist/prompts/shared/tool-usage.d.ts +2 -0
  59. package/dist/prompts/shared/tool-usage.d.ts.map +1 -1
  60. package/dist/prompts/shared/tool-usage.js +35 -1
  61. package/dist/prompts/shared/tool-usage.js.map +1 -1
  62. package/dist/prompts/system/plan-execute.d.ts +3 -0
  63. package/dist/prompts/system/plan-execute.d.ts.map +1 -1
  64. package/dist/prompts/system/plan-execute.js +49 -19
  65. package/dist/prompts/system/plan-execute.js.map +1 -1
  66. package/dist/tools/browser/browser-client.d.ts +0 -2
  67. package/dist/tools/browser/browser-client.d.ts.map +1 -1
  68. package/dist/tools/browser/browser-client.js +6 -12
  69. package/dist/tools/browser/browser-client.js.map +1 -1
  70. package/dist/tools/browser/browser-tools.js +2 -2
  71. package/dist/tools/browser/browser-tools.js.map +1 -1
  72. package/dist/tools/llm/index.d.ts +0 -1
  73. package/dist/tools/llm/index.d.ts.map +1 -1
  74. package/dist/tools/llm/index.js +0 -1
  75. package/dist/tools/llm/index.js.map +1 -1
  76. package/dist/tools/llm/simple/bash-tool.js +1 -1
  77. package/dist/tools/llm/simple/bash-tool.js.map +1 -1
  78. package/dist/tools/llm/simple/external-services/external-service-api-clients.d.ts +54 -0
  79. package/dist/tools/llm/simple/external-services/external-service-api-clients.d.ts.map +1 -0
  80. package/dist/tools/llm/simple/external-services/external-service-api-clients.js +208 -0
  81. package/dist/tools/llm/simple/external-services/external-service-api-clients.js.map +1 -0
  82. package/dist/tools/llm/simple/external-services/free-tools.d.ts +2 -0
  83. package/dist/tools/llm/simple/external-services/free-tools.d.ts.map +1 -0
  84. package/dist/tools/llm/simple/external-services/free-tools.js +2 -0
  85. package/dist/tools/llm/simple/external-services/free-tools.js.map +1 -0
  86. package/dist/tools/llm/simple/external-services/index.d.ts +3 -0
  87. package/dist/tools/llm/simple/external-services/index.d.ts.map +1 -0
  88. package/dist/tools/llm/simple/external-services/index.js +3 -0
  89. package/dist/tools/llm/simple/external-services/index.js.map +1 -0
  90. package/dist/tools/llm/simple/external-services/once-tools.d.ts +3 -0
  91. package/dist/tools/llm/simple/external-services/once-tools.d.ts.map +1 -0
  92. package/dist/tools/llm/simple/external-services/once-tools.js +106 -0
  93. package/dist/tools/llm/simple/external-services/once-tools.js.map +1 -0
  94. package/dist/tools/llm/simple/file-tools.d.ts +1 -1
  95. package/dist/tools/llm/simple/file-tools.d.ts.map +1 -1
  96. package/dist/tools/llm/simple/file-tools.js +49 -6
  97. package/dist/tools/llm/simple/file-tools.js.map +1 -1
  98. package/dist/tools/llm/simple/index.d.ts +1 -0
  99. package/dist/tools/llm/simple/index.d.ts.map +1 -1
  100. package/dist/tools/llm/simple/index.js +1 -0
  101. package/dist/tools/llm/simple/index.js.map +1 -1
  102. package/dist/tools/llm/simple/powershell-tool.d.ts.map +1 -1
  103. package/dist/tools/llm/simple/powershell-tool.js +16 -3
  104. package/dist/tools/llm/simple/powershell-tool.js.map +1 -1
  105. package/dist/tools/llm/simple/simple-tool-executor.d.ts +3 -0
  106. package/dist/tools/llm/simple/simple-tool-executor.d.ts.map +1 -1
  107. package/dist/tools/llm/simple/simple-tool-executor.js +9 -0
  108. package/dist/tools/llm/simple/simple-tool-executor.js.map +1 -1
  109. package/dist/tools/llm/simple/todo-tools.js +1 -1
  110. package/dist/tools/llm/simple/todo-tools.js.map +1 -1
  111. package/dist/tools/llm/simple/user-interaction-tools.js +1 -1
  112. package/dist/tools/llm/simple/user-interaction-tools.js.map +1 -1
  113. package/dist/tools/office/common/constants.d.ts +0 -2
  114. package/dist/tools/office/common/constants.d.ts.map +1 -1
  115. package/dist/tools/office/common/constants.js +0 -4
  116. package/dist/tools/office/common/constants.js.map +1 -1
  117. package/dist/tools/office/common/index.d.ts +1 -1
  118. package/dist/tools/office/common/index.d.ts.map +1 -1
  119. package/dist/tools/office/common/index.js +1 -1
  120. package/dist/tools/office/common/index.js.map +1 -1
  121. package/dist/tools/office/common/utils.d.ts.map +1 -1
  122. package/dist/tools/office/common/utils.js +2 -4
  123. package/dist/tools/office/common/utils.js.map +1 -1
  124. package/dist/tools/office/excel-client.d.ts +1 -0
  125. package/dist/tools/office/excel-client.d.ts.map +1 -1
  126. package/dist/tools/office/excel-client.js +7 -4
  127. package/dist/tools/office/excel-client.js.map +1 -1
  128. package/dist/tools/office/excel-tools/cells.d.ts.map +1 -1
  129. package/dist/tools/office/excel-tools/cells.js +5 -1
  130. package/dist/tools/office/excel-tools/cells.js.map +1 -1
  131. package/dist/tools/office/excel-tools/launch.js +3 -3
  132. package/dist/tools/office/excel-tools/launch.js.map +1 -1
  133. package/dist/tools/office/excel-tools/sheets.d.ts.map +1 -1
  134. package/dist/tools/office/excel-tools/sheets.js +6 -1
  135. package/dist/tools/office/excel-tools/sheets.js.map +1 -1
  136. package/dist/tools/office/excel-tools/tables.d.ts.map +1 -1
  137. package/dist/tools/office/excel-tools/tables.js +6 -1
  138. package/dist/tools/office/excel-tools/tables.js.map +1 -1
  139. package/dist/tools/office/office-client-base.d.ts +1 -0
  140. package/dist/tools/office/office-client-base.d.ts.map +1 -1
  141. package/dist/tools/office/office-client-base.js +12 -1
  142. package/dist/tools/office/office-client-base.js.map +1 -1
  143. package/dist/tools/office/powerpoint-client.d.ts +1 -0
  144. package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
  145. package/dist/tools/office/powerpoint-client.js +9 -1
  146. package/dist/tools/office/powerpoint-client.js.map +1 -1
  147. package/dist/tools/office/powerpoint-tools/launch.js +3 -3
  148. package/dist/tools/office/powerpoint-tools/launch.js.map +1 -1
  149. package/dist/tools/office/word-client.d.ts +1 -0
  150. package/dist/tools/office/word-client.d.ts.map +1 -1
  151. package/dist/tools/office/word-client.js +7 -2
  152. package/dist/tools/office/word-client.js.map +1 -1
  153. package/dist/tools/office/word-tools/launch.js +3 -3
  154. package/dist/tools/office/word-tools/launch.js.map +1 -1
  155. package/dist/tools/registry.d.ts.map +1 -1
  156. package/dist/tools/registry.js +8 -5
  157. package/dist/tools/registry.js.map +1 -1
  158. package/dist/ui/TodoPanel.d.ts.map +1 -1
  159. package/dist/ui/TodoPanel.js +7 -4
  160. package/dist/ui/TodoPanel.js.map +1 -1
  161. package/dist/ui/components/ActivityIndicator.d.ts +1 -1
  162. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  163. package/dist/ui/components/ActivityIndicator.js +5 -4
  164. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  165. package/dist/ui/components/CommandBrowser.d.ts.map +1 -1
  166. package/dist/ui/components/CommandBrowser.js +7 -3
  167. package/dist/ui/components/CommandBrowser.js.map +1 -1
  168. package/dist/ui/components/CustomTextInput.d.ts.map +1 -1
  169. package/dist/ui/components/CustomTextInput.js +2 -1
  170. package/dist/ui/components/CustomTextInput.js.map +1 -1
  171. package/dist/ui/components/PlanExecuteApp.d.ts +1 -1
  172. package/dist/ui/components/PlanExecuteApp.d.ts.map +1 -1
  173. package/dist/ui/components/PlanExecuteApp.js +41 -87
  174. package/dist/ui/components/PlanExecuteApp.js.map +1 -1
  175. package/dist/ui/components/dialogs/ApprovalDialog.d.ts.map +1 -1
  176. package/dist/ui/components/dialogs/ApprovalDialog.js +5 -3
  177. package/dist/ui/components/dialogs/ApprovalDialog.js.map +1 -1
  178. package/dist/ui/components/dialogs/DocsBrowser.js +2 -2
  179. package/dist/ui/components/dialogs/DocsBrowser.js.map +1 -1
  180. package/dist/ui/hooks/index.d.ts +1 -0
  181. package/dist/ui/hooks/index.d.ts.map +1 -1
  182. package/dist/ui/hooks/index.js +1 -0
  183. package/dist/ui/hooks/index.js.map +1 -1
  184. package/dist/ui/hooks/useFileBrowserState.js +1 -1
  185. package/dist/ui/hooks/useFileBrowserState.js.map +1 -1
  186. package/dist/ui/hooks/useFileList.js +2 -2
  187. package/dist/ui/hooks/useFileList.js.map +1 -1
  188. package/dist/ui/hooks/useTerminalWidth.d.ts +4 -0
  189. package/dist/ui/hooks/useTerminalWidth.d.ts.map +1 -0
  190. package/dist/ui/hooks/useTerminalWidth.js +26 -0
  191. package/dist/ui/hooks/useTerminalWidth.js.map +1 -0
  192. package/dist/ui/ink-entry.js +1 -1
  193. package/dist/ui/ink-entry.js.map +1 -1
  194. package/dist/utils/json-stream-logger.js +0 -2
  195. package/dist/utils/json-stream-logger.js.map +1 -1
  196. package/dist/utils/logger.d.ts +4 -0
  197. package/dist/utils/logger.d.ts.map +1 -1
  198. package/dist/utils/logger.js +18 -0
  199. package/dist/utils/logger.js.map +1 -1
  200. 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
- return messages.map((msg) => {
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
- let processedMsg = { ...msg };
44
- if (msgAny.reasoning_content && (!msg.content || msg.content.trim() === '')) {
45
- processedMsg.content = msgAny.reasoning_content;
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.error('Invalid response structure - missing choices array', response.data);
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.error(`LLM 호출 ${maxRetries}번 재시도 후 최종 실패`, {
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
- throw this.handleError(error, {
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
- throw this.handleError(error, {
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
- workingMessages.push({ role: 'user', content: pendingMsg });
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 with compact');
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.error('Compact failed during recovery', { error: compactResult.error });
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
- workingMessages.push(assistantMessage);
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 toolName = toolCall.function.name;
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
- const errorMsg = `Tool argument parsing failed for ${toolName}`;
469
- logger.error(errorMsg, parseError);
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
- workingMessages.push({
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: `Error: Failed to parse tool arguments - ${parseError instanceof Error ? parseError.message : 'Unknown error'}`,
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
- workingMessages.push({
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
- workingMessages.push({
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: workingMessages,
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: workingMessages,
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
- workingMessages.push({
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: workingMessages,
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
- workingMessages.push({
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.error('LLM Client Error', error);
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.error('Request Timeout', {
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.error('=== API ERROR DETAILS ===', {
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.error('Context Length Exceeded', {
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.error('Token Limit Error', {
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.error('Quota Exceeded', {
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.error('Rate Limit Exceeded', {
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.error('Authentication Failed', {
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.error('Access Forbidden', {
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.error('Endpoint Not Found', {
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.error('Server Error', {
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.error('API Error', {
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.error('Network Error - No Response', {
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.error('Axios Error', {
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.error('Unexpected Error', error);
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.error('Unknown Error Type', { error });
1038
+ logger.errorSilent('Unknown Error Type', { error });
842
1039
  return new LLMError('알 수 없는 에러가 발생했습니다.', {
843
1040
  details: { unknownError: error },
844
1041
  });