aiexecode 1.0.94 → 1.0.98

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.

Files changed (60) hide show
  1. package/README.md +198 -88
  2. package/index.js +43 -9
  3. package/package.json +4 -4
  4. package/payload_viewer/out/404/index.html +1 -1
  5. package/payload_viewer/out/404.html +1 -1
  6. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  7. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  8. package/payload_viewer/out/index.html +1 -1
  9. package/payload_viewer/out/index.txt +3 -3
  10. package/payload_viewer/web_server.js +361 -0
  11. package/src/LLMClient/client.js +392 -16
  12. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  13. package/src/LLMClient/converters/responses-to-zai.js +608 -0
  14. package/src/LLMClient/errors.js +30 -4
  15. package/src/LLMClient/index.js +5 -0
  16. package/src/ai_based/completion_judge.js +35 -4
  17. package/src/ai_based/orchestrator.js +146 -35
  18. package/src/commands/agents.js +70 -0
  19. package/src/commands/apikey.js +1 -1
  20. package/src/commands/commands.js +51 -0
  21. package/src/commands/debug.js +52 -0
  22. package/src/commands/help.js +11 -1
  23. package/src/commands/model.js +42 -7
  24. package/src/commands/reasoning_effort.js +2 -2
  25. package/src/commands/skills.js +46 -0
  26. package/src/config/ai_models.js +106 -6
  27. package/src/config/constants.js +71 -0
  28. package/src/frontend/App.js +8 -0
  29. package/src/frontend/components/AutocompleteMenu.js +7 -1
  30. package/src/frontend/components/CurrentModelView.js +2 -2
  31. package/src/frontend/components/HelpView.js +106 -2
  32. package/src/frontend/components/Input.js +33 -11
  33. package/src/frontend/components/ModelListView.js +1 -1
  34. package/src/frontend/components/SetupWizard.js +51 -8
  35. package/src/frontend/hooks/useFileCompletion.js +467 -0
  36. package/src/frontend/utils/toolUIFormatter.js +261 -0
  37. package/src/system/agents_loader.js +289 -0
  38. package/src/system/ai_request.js +175 -12
  39. package/src/system/command_parser.js +33 -3
  40. package/src/system/conversation_state.js +265 -0
  41. package/src/system/custom_command_loader.js +386 -0
  42. package/src/system/session.js +59 -35
  43. package/src/system/skill_loader.js +318 -0
  44. package/src/system/tool_approval.js +10 -0
  45. package/src/tools/file_reader.js +49 -9
  46. package/src/tools/glob.js +0 -3
  47. package/src/tools/ripgrep.js +5 -7
  48. package/src/tools/skill_tool.js +122 -0
  49. package/src/tools/web_downloader.js +0 -3
  50. package/src/util/clone.js +174 -0
  51. package/src/util/config.js +38 -2
  52. package/src/util/config_migration.js +174 -0
  53. package/src/util/file_reference_parser.js +132 -0
  54. package/src/util/path_validator.js +178 -0
  55. package/src/util/prompt_loader.js +68 -1
  56. package/src/util/safe_fs.js +43 -3
  57. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  58. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_buildManifest.js +0 -0
  59. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_clientMiddlewareManifest.json +0 -0
  60. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → WjvWEjPqhHNIE_a6QIZaG}/_ssgManifest.js +0 -0
@@ -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 (first 10 chars): ' + process.env.API_KEY.substring(0, 10) + '...');
60
- debugLog(`[getLLMClient] Model: ${currentModel}, Provider: ${provider}`);
61
- llmClient = new UnifiedLLMClient({
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 = JSON.parse(output);
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
- parsedOutput.stdout = JSON.parse(parsedOutput.stdout);
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
- debugLog(`[request] Calling client.response with abort signal`);
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;
@@ -566,21 +704,46 @@ export async function request(taskName, requestPayload) {
566
704
  *
567
705
  * @see https://platform.openai.com/docs/guides/error-codes
568
706
  */
707
+ /*
708
+ * Z.AI/GLM 컨텍스트 초과 에러 샘플:
709
+ * {
710
+ * "error": {
711
+ * "code": "1210",
712
+ * "message": "Invalid API parameter, please check the documentation.Request 320006 input tokens exceeds the model's maximum context length 202750"
713
+ * },
714
+ * "request_id": "20260124000100026f0914d2e744f5"
715
+ * }
716
+ * HTTP_STATUS: 400
717
+ */
569
718
  export function shouldRetryWithTrim(error) {
570
719
  if (!error) return false;
571
720
 
572
- // OpenAI SDK의 공식 에러 코드 확인
721
+ // 에러 코드 메시지 추출
573
722
  const errorCode = error?.code || error?.error?.code;
723
+ const errorMessage = error?.message || error?.error?.message || '';
574
724
 
725
+ // OpenAI: context_length_exceeded
575
726
  if (errorCode === 'context_length_exceeded') {
576
- debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded');
727
+ debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded (OpenAI)');
728
+ return true;
729
+ }
730
+
731
+ // Z.AI/GLM: code "1210" (context length exceeded)
732
+ if (errorCode === '1210' || errorCode === 1210) {
733
+ debugLog('[shouldRetryWithTrim] Detected: code 1210 (GLM context exceeded)');
734
+ return true;
735
+ }
736
+
737
+ // 메시지에서 컨텍스트 초과 패턴 감지 (다양한 API 대응)
738
+ const contextExceededPattern = /exceeds.*(?:context|token).*(?:length|limit|maximum)/i;
739
+ if (contextExceededPattern.test(errorMessage)) {
740
+ debugLog(`[shouldRetryWithTrim] Detected context exceeded in message: ${errorMessage.substring(0, 100)}`);
577
741
  return true;
578
742
  }
579
743
 
580
744
  // 400 에러도 trim 후 재시도 대상
581
745
  if (error?.status === 400 || error?.response?.status === 400) {
582
746
  const errorType = error?.type || error?.error?.type;
583
- const errorMessage = error?.message || error?.error?.message || '';
584
747
  debugLog(
585
748
  `[shouldRetryWithTrim] Detected 400 error - ` +
586
749
  `Type: ${errorType}, Code: ${errorCode}, ` +
@@ -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 (!commandConfig) {
81
- throw new Error(`Unknown command: ${parsed.command}`);
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
- return await commandConfig.handler(parsed.args, parsed);
114
+ throw new Error(`Unknown command: ${parsed.command}`);
85
115
  }
86
116
 
87
117
  /**
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Conversation State Manager
3
+ * orchestratorConversation 전역 변수를 클래스로 캡슐화하여 상태 관리를 개선합니다.
4
+ */
5
+
6
+ import { deepClone } from '../util/clone.js';
7
+
8
+ /**
9
+ * 대화 상태 관리 클래스
10
+ * 전역 대화 상태를 캡슐화하고 안전한 접근 메서드를 제공합니다.
11
+ */
12
+ class ConversationStateManager {
13
+ constructor() {
14
+ /** @type {Array} 대화 히스토리 */
15
+ this._conversation = [];
16
+
17
+ /** @type {Object|null} 요청 옵션 */
18
+ this._requestOptions = null;
19
+
20
+ /** @type {number} 마지막 수정 타임스탬프 */
21
+ this._lastModified = Date.now();
22
+
23
+ /** @type {string|null} 현재 세션 ID */
24
+ this._sessionId = null;
25
+ }
26
+
27
+ /**
28
+ * 대화 히스토리를 가져옵니다.
29
+ * @param {boolean} clone - true이면 깊은 복사본 반환 (기본: false)
30
+ * @returns {Array} 대화 히스토리
31
+ */
32
+ getConversation(clone = false) {
33
+ if (clone) {
34
+ return deepClone(this._conversation);
35
+ }
36
+ return this._conversation;
37
+ }
38
+
39
+ /**
40
+ * 대화 히스토리를 설정합니다.
41
+ * @param {Array} conversation - 새 대화 히스토리
42
+ */
43
+ setConversation(conversation) {
44
+ if (!Array.isArray(conversation)) {
45
+ throw new Error('Conversation must be an array');
46
+ }
47
+ this._conversation = conversation;
48
+ this._lastModified = Date.now();
49
+ }
50
+
51
+ /**
52
+ * 대화에 메시지를 추가합니다.
53
+ * @param {Object} message - 추가할 메시지
54
+ */
55
+ addMessage(message) {
56
+ if (!message || typeof message !== 'object') {
57
+ throw new Error('Message must be an object');
58
+ }
59
+ this._conversation.push(message);
60
+ this._lastModified = Date.now();
61
+ }
62
+
63
+ /**
64
+ * 대화에 여러 메시지를 추가합니다.
65
+ * @param {Array} messages - 추가할 메시지 배열
66
+ */
67
+ addMessages(messages) {
68
+ if (!Array.isArray(messages)) {
69
+ throw new Error('Messages must be an array');
70
+ }
71
+ this._conversation.push(...messages);
72
+ this._lastModified = Date.now();
73
+ }
74
+
75
+ /**
76
+ * 대화 히스토리를 초기화합니다.
77
+ */
78
+ reset() {
79
+ this._conversation = [];
80
+ this._requestOptions = null;
81
+ this._lastModified = Date.now();
82
+ }
83
+
84
+ /**
85
+ * 대화 히스토리를 복원합니다.
86
+ * @param {Array} conversation - 복원할 대화 히스토리
87
+ * @param {Object} requestOptions - 복원할 요청 옵션
88
+ */
89
+ restore(conversation, requestOptions = null) {
90
+ this._conversation = Array.isArray(conversation) ? conversation : [];
91
+ this._requestOptions = requestOptions;
92
+ this._lastModified = Date.now();
93
+ }
94
+
95
+ /**
96
+ * 요청 옵션을 가져옵니다.
97
+ * @returns {Object|null} 요청 옵션
98
+ */
99
+ getRequestOptions() {
100
+ return this._requestOptions;
101
+ }
102
+
103
+ /**
104
+ * 요청 옵션을 설정합니다.
105
+ * @param {Object} options - 요청 옵션
106
+ */
107
+ setRequestOptions(options) {
108
+ this._requestOptions = options;
109
+ this._lastModified = Date.now();
110
+ }
111
+
112
+ /**
113
+ * 대화 히스토리의 길이를 반환합니다.
114
+ * @returns {number} 메시지 수
115
+ */
116
+ get length() {
117
+ return this._conversation.length;
118
+ }
119
+
120
+ /**
121
+ * 대화가 비어있는지 확인합니다.
122
+ * @returns {boolean} 비어있으면 true
123
+ */
124
+ isEmpty() {
125
+ return this._conversation.length === 0;
126
+ }
127
+
128
+ /**
129
+ * 마지막 수정 시간을 반환합니다.
130
+ * @returns {number} 타임스탬프
131
+ */
132
+ getLastModified() {
133
+ return this._lastModified;
134
+ }
135
+
136
+ /**
137
+ * 세션 ID를 설정합니다.
138
+ * @param {string} sessionId - 세션 ID
139
+ */
140
+ setSessionId(sessionId) {
141
+ this._sessionId = sessionId;
142
+ }
143
+
144
+ /**
145
+ * 세션 ID를 가져옵니다.
146
+ * @returns {string|null} 세션 ID
147
+ */
148
+ getSessionId() {
149
+ return this._sessionId;
150
+ }
151
+
152
+ /**
153
+ * 대화에서 특정 조건을 만족하는 메시지를 찾습니다.
154
+ * @param {Function} predicate - 검색 조건 함수
155
+ * @returns {Object|undefined} 찾은 메시지 또는 undefined
156
+ */
157
+ findMessage(predicate) {
158
+ return this._conversation.find(predicate);
159
+ }
160
+
161
+ /**
162
+ * 대화에서 특정 조건을 만족하는 메시지들을 필터링합니다.
163
+ * @param {Function} predicate - 필터 조건 함수
164
+ * @returns {Array} 필터링된 메시지 배열
165
+ */
166
+ filterMessages(predicate) {
167
+ return this._conversation.filter(predicate);
168
+ }
169
+
170
+ /**
171
+ * 마지막 메시지를 가져옵니다.
172
+ * @returns {Object|undefined} 마지막 메시지 또는 undefined
173
+ */
174
+ getLastMessage() {
175
+ if (this._conversation.length === 0) {
176
+ return undefined;
177
+ }
178
+ return this._conversation[this._conversation.length - 1];
179
+ }
180
+
181
+ /**
182
+ * 마지막 N개의 메시지를 가져옵니다.
183
+ * @param {number} count - 가져올 메시지 수
184
+ * @returns {Array} 메시지 배열
185
+ */
186
+ getLastMessages(count) {
187
+ if (count <= 0) {
188
+ return [];
189
+ }
190
+ return this._conversation.slice(-count);
191
+ }
192
+
193
+ /**
194
+ * 대화 히스토리를 트리밍합니다 (오래된 메시지 제거).
195
+ * @param {number} keepCount - 유지할 메시지 수 (시스템 메시지 제외)
196
+ * @returns {number} 제거된 메시지 수
197
+ */
198
+ trim(keepCount) {
199
+ const originalLength = this._conversation.length;
200
+
201
+ // 시스템 메시지는 유지
202
+ const systemMessages = this._conversation.filter(m => m.role === 'system');
203
+ const otherMessages = this._conversation.filter(m => m.role !== 'system');
204
+
205
+ // 최근 keepCount개만 유지
206
+ const keptMessages = otherMessages.slice(-keepCount);
207
+
208
+ this._conversation = [...systemMessages, ...keptMessages];
209
+ this._lastModified = Date.now();
210
+
211
+ return originalLength - this._conversation.length;
212
+ }
213
+
214
+ /**
215
+ * 상태를 직렬화 가능한 객체로 변환합니다.
216
+ * @returns {Object} 직렬화 가능한 상태 객체
217
+ */
218
+ toJSON() {
219
+ return {
220
+ conversation: deepClone(this._conversation),
221
+ requestOptions: this._requestOptions ? deepClone(this._requestOptions) : null,
222
+ lastModified: this._lastModified,
223
+ sessionId: this._sessionId
224
+ };
225
+ }
226
+
227
+ /**
228
+ * 직렬화된 상태에서 복원합니다.
229
+ * @param {Object} state - 직렬화된 상태 객체
230
+ */
231
+ fromJSON(state) {
232
+ if (state.conversation) {
233
+ this._conversation = state.conversation;
234
+ }
235
+ if (state.requestOptions !== undefined) {
236
+ this._requestOptions = state.requestOptions;
237
+ }
238
+ if (state.lastModified) {
239
+ this._lastModified = state.lastModified;
240
+ }
241
+ if (state.sessionId) {
242
+ this._sessionId = state.sessionId;
243
+ }
244
+ }
245
+ }
246
+
247
+ // 싱글톤 인스턴스 생성 및 export
248
+ export const conversationState = new ConversationStateManager();
249
+
250
+ // 후방 호환성을 위한 개별 함수들 export
251
+ export function getOrchestratorConversation() {
252
+ return conversationState.getConversation();
253
+ }
254
+
255
+ export function setOrchestratorConversation(conversation) {
256
+ conversationState.setConversation(conversation);
257
+ }
258
+
259
+ export function resetOrchestratorConversation() {
260
+ conversationState.reset();
261
+ }
262
+
263
+ export function restoreOrchestratorConversation(conversation, requestOptions) {
264
+ conversationState.restore(conversation, requestOptions);
265
+ }