aiexecode 1.0.94 → 1.0.127

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 (80) hide show
  1. package/README.md +198 -88
  2. package/index.js +310 -86
  3. package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
  4. package/package.json +4 -4
  5. package/payload_viewer/out/404/index.html +1 -1
  6. package/payload_viewer/out/404.html +1 -1
  7. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  8. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  9. package/payload_viewer/out/index.html +1 -1
  10. package/payload_viewer/out/index.txt +3 -3
  11. package/payload_viewer/web_server.js +361 -0
  12. package/prompts/completion_judge.txt +4 -0
  13. package/prompts/orchestrator.txt +116 -3
  14. package/src/LLMClient/client.js +401 -18
  15. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  16. package/src/LLMClient/converters/responses-to-zai.js +667 -0
  17. package/src/LLMClient/errors.js +30 -4
  18. package/src/LLMClient/index.js +5 -0
  19. package/src/ai_based/completion_judge.js +263 -186
  20. package/src/ai_based/orchestrator.js +171 -35
  21. package/src/commands/agents.js +70 -0
  22. package/src/commands/apikey.js +1 -1
  23. package/src/commands/bg.js +129 -0
  24. package/src/commands/commands.js +51 -0
  25. package/src/commands/debug.js +52 -0
  26. package/src/commands/help.js +11 -1
  27. package/src/commands/model.js +42 -7
  28. package/src/commands/reasoning_effort.js +2 -2
  29. package/src/commands/skills.js +46 -0
  30. package/src/config/ai_models.js +106 -6
  31. package/src/config/constants.js +71 -0
  32. package/src/config/feature_flags.js +6 -7
  33. package/src/frontend/App.js +108 -1
  34. package/src/frontend/components/AutocompleteMenu.js +7 -1
  35. package/src/frontend/components/BackgroundProcessList.js +175 -0
  36. package/src/frontend/components/ConversationItem.js +26 -10
  37. package/src/frontend/components/CurrentModelView.js +2 -2
  38. package/src/frontend/components/HelpView.js +106 -2
  39. package/src/frontend/components/Input.js +33 -11
  40. package/src/frontend/components/ModelListView.js +1 -1
  41. package/src/frontend/components/SetupWizard.js +51 -8
  42. package/src/frontend/hooks/useFileCompletion.js +467 -0
  43. package/src/frontend/utils/toolUIFormatter.js +261 -0
  44. package/src/system/agents_loader.js +289 -0
  45. package/src/system/ai_request.js +156 -12
  46. package/src/system/background_process.js +317 -0
  47. package/src/system/code_executer.js +496 -56
  48. package/src/system/command_parser.js +33 -3
  49. package/src/system/conversation_state.js +265 -0
  50. package/src/system/conversation_trimmer.js +132 -0
  51. package/src/system/custom_command_loader.js +386 -0
  52. package/src/system/file_integrity.js +73 -10
  53. package/src/system/log.js +10 -2
  54. package/src/system/output_helper.js +52 -9
  55. package/src/system/session.js +213 -58
  56. package/src/system/session_memory.js +30 -2
  57. package/src/system/skill_loader.js +318 -0
  58. package/src/system/system_info.js +254 -40
  59. package/src/system/tool_approval.js +10 -0
  60. package/src/system/tool_registry.js +15 -1
  61. package/src/system/ui_events.js +11 -0
  62. package/src/tools/code_editor.js +16 -10
  63. package/src/tools/file_reader.js +66 -9
  64. package/src/tools/glob.js +0 -3
  65. package/src/tools/ripgrep.js +5 -7
  66. package/src/tools/skill_tool.js +122 -0
  67. package/src/tools/web_downloader.js +0 -3
  68. package/src/util/clone.js +174 -0
  69. package/src/util/config.js +55 -2
  70. package/src/util/config_migration.js +174 -0
  71. package/src/util/debug_log.js +8 -2
  72. package/src/util/exit_handler.js +8 -0
  73. package/src/util/file_reference_parser.js +132 -0
  74. package/src/util/path_validator.js +178 -0
  75. package/src/util/prompt_loader.js +91 -1
  76. package/src/util/safe_fs.js +66 -3
  77. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  78. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
  79. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
  80. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
@@ -176,15 +176,41 @@ export function normalizeError(error, provider) {
176
176
  }
177
177
 
178
178
  // Anthropic SDK error (check BEFORE OpenAI SDK error, as both have error.status && error.error)
179
- if (provider === 'claude' && error.status) {
179
+ // Z.AI uses Anthropic SDK so handle similarly
180
+ if ((provider === 'claude' || provider === 'zai') && error.status) {
180
181
  // Parse Claude error from error.error object (Anthropic SDK provides parsed error)
181
182
  let errorType = ERROR_TYPE_MAP[error.status] || 'api_error';
182
183
  let code = null;
183
- let message = error.message || 'Claude API error';
184
+ let message = error.message || (provider === 'zai' ? 'Z.AI API error' : 'Claude API error');
184
185
 
186
+ // Z.AI specific error format: { detail: [{ type, loc, msg, ... }] }
187
+ if (error.error && error.error.detail && Array.isArray(error.error.detail)) {
188
+ const zaiError = error.error.detail[0];
189
+ if (zaiError) {
190
+ code = zaiError.type || null;
191
+ message = zaiError.msg || message;
192
+
193
+ // Map Z.AI error types
194
+ if (zaiError.type === 'json_invalid') {
195
+ errorType = 'invalid_request_error';
196
+ }
197
+ }
198
+ }
199
+ // Z.AI auth error format: { error: { message, type: "401" } }
200
+ else if (error.error && error.error.type === '401') {
201
+ errorType = 'authentication_error';
202
+ code = 'invalid_api_key';
203
+ message = error.error.message || 'Invalid or expired API key';
204
+ }
205
+ // Z.AI context exceeded format: { error: { code: "1210", message: "..." } }
206
+ else if (error.error && (error.error.code === '1210' || error.error.code === 1210)) {
207
+ errorType = 'invalid_request_error';
208
+ code = 'context_length_exceeded';
209
+ message = error.error.message || 'Context length exceeded';
210
+ }
185
211
  // Anthropic SDK provides error.error object with structure:
186
212
  // { type: 'error', error: { type: 'not_found_error', message: '...' } }
187
- if (error.error && error.error.error) {
213
+ else if (error.error && error.error.error) {
188
214
  const claudeError = error.error.error;
189
215
 
190
216
  if (claudeError.type) {
@@ -220,7 +246,7 @@ export function normalizeError(error, provider) {
220
246
  param: null,
221
247
  code: code,
222
248
  status: error.status,
223
- provider: 'claude',
249
+ provider: provider,
224
250
  originalError: error
225
251
  }
226
252
  );
@@ -21,6 +21,11 @@ export {
21
21
  convertOllamaResponseToResponsesFormat
22
22
  } from './converters/responses-to-ollama.js';
23
23
 
24
+ export {
25
+ convertResponsesRequestToZaiFormat,
26
+ convertZaiResponseToResponsesFormat
27
+ } from './converters/responses-to-zai.js';
28
+
24
29
  // Export error classes
25
30
  export {
26
31
  LLMError,
@@ -1,10 +1,9 @@
1
1
  import dotenv from "dotenv";
2
- import { request, shouldRetryWithTrim, getModelForProvider } from "../system/ai_request.js";
2
+ import { request, getModelForProvider } from "../system/ai_request.js";
3
3
  import { getOrchestratorConversation } from "./orchestrator.js";
4
4
  import { createSystemMessage } from "../util/prompt_loader.js";
5
5
  import { createDebugLogger } from "../util/debug_log.js";
6
6
  import { getCurrentTodos } from "../system/session_memory.js";
7
- import { cleanupOrphanOutputs, trimConversation } from "../system/conversation_trimmer.js";
8
7
 
9
8
  dotenv.config({ quiet: true });
10
9
 
@@ -30,16 +29,6 @@ export const completionJudgmentSchema = {
30
29
  strict: true
31
30
  };
32
31
 
33
- // systemMessage는 judgeMissionCompletion 호출 시 동적으로 생성됨
34
-
35
- const completionJudgeConversation = [];
36
- let lastOrchestratorSnapshotLength = 0;
37
-
38
- function cloneMessage(message) {
39
- return JSON.parse(JSON.stringify(message));
40
- }
41
-
42
-
43
32
  async function createCompletionJudgeRequestOptions() {
44
33
  const model = await getModelForProvider();
45
34
  return {
@@ -50,76 +39,150 @@ async function createCompletionJudgeRequestOptions() {
50
39
  }
51
40
 
52
41
  /**
53
- * Completion judge conversation을 초기화하거나 시스템 프롬프트를 업데이트합니다.
54
- * 요청마다 프롬프트 파일을 새로 읽어서 변경사항을 즉시 반영합니다.
42
+ * Orchestrator 대화에서 요약된 컨텍스트를 추출합니다.
43
+ * 전체 대화 대신 핵심 정보만 텍스트로 요약하여 반환합니다.
44
+ *
45
+ * @returns {Object} { toolExecutionSummary: string, lastAssistantMessage: string }
55
46
  */
56
- async function ensureCompletionJudgeConversationInitialized(templateVars = {}) {
57
- // 매번 최신 시스템 프롬프트를 로드
58
- const systemMessage = await createSystemMessage("completion_judge.txt", templateVars);
59
-
60
- const systemMessageEntry = {
61
- role: "system",
62
- content: [
63
- {
64
- type: "input_text",
65
- text: systemMessage.content
47
+ function extractSummarizedContext() {
48
+ debugLog(`[extractSummarizedContext] START`);
49
+ const sourceConversation = getOrchestratorConversation();
50
+
51
+ if (!Array.isArray(sourceConversation) || !sourceConversation.length) {
52
+ debugLog(`[extractSummarizedContext] No source conversation`);
53
+ return { toolExecutionSummary: '', lastAssistantMessage: '' };
54
+ }
55
+
56
+ debugLog(`[extractSummarizedContext] Source conversation length: ${sourceConversation.length}`);
57
+
58
+ // 도구 실행 결과 추출
59
+ const toolExecutions = [];
60
+ const functionCalls = new Map(); // call_id -> { name, arguments }
61
+
62
+ for (const entry of sourceConversation) {
63
+ if (!entry) continue;
64
+
65
+ // function_call 정보 수집
66
+ if (entry.type === 'function_call') {
67
+ functionCalls.set(entry.call_id || entry.id, {
68
+ name: entry.name,
69
+ arguments: entry.arguments
70
+ });
71
+ }
72
+
73
+ // function_call_output에서 결과 추출
74
+ if (entry.type === 'function_call_output') {
75
+ const callId = entry.call_id;
76
+ const callInfo = functionCalls.get(callId);
77
+ const toolName = callInfo?.name || 'unknown';
78
+
79
+ let success = true;
80
+ let exitCode = null;
81
+ let errorMessage = '';
82
+
83
+ try {
84
+ const output = JSON.parse(entry.output || '{}');
85
+ if (output.exit_code !== undefined) {
86
+ exitCode = output.exit_code;
87
+ success = exitCode === 0;
88
+ }
89
+ if (output.operation_successful === false) {
90
+ success = false;
91
+ }
92
+ if (output.error_message) {
93
+ errorMessage = output.error_message;
94
+ }
95
+ if (output.stderr && output.stderr.trim()) {
96
+ errorMessage = output.stderr.substring(0, 100);
97
+ }
98
+ } catch (e) {
99
+ // JSON 파싱 실패 시 무시
100
+ }
101
+
102
+ // 인자에서 주요 정보 추출 (파일 경로 등)
103
+ let targetInfo = '';
104
+ if (callInfo?.arguments) {
105
+ try {
106
+ const args = JSON.parse(callInfo.arguments);
107
+ if (args.file_path) targetInfo = ` (${args.file_path})`;
108
+ else if (args.path) targetInfo = ` (${args.path})`;
109
+ else if (args.script) targetInfo = ` (bash)`;
110
+ else if (args.code) targetInfo = ` (python)`;
111
+ } catch (e) {}
66
112
  }
67
- ]
68
- };
69
113
 
70
- // conversation이 비어있으면 새로 추가
71
- if (!completionJudgeConversation.length) {
72
- completionJudgeConversation.push(systemMessageEntry);
73
- } else {
74
- // conversation이 있으면 첫 번째 system 메시지를 업데이트
75
- if (completionJudgeConversation[0]?.role === "system") {
76
- completionJudgeConversation[0] = systemMessageEntry;
77
- } else {
78
- // system 메시지가 맨 앞에 없으면 추가
79
- completionJudgeConversation.unshift(systemMessageEntry);
114
+ const status = success ? '✓' : '✗';
115
+ const exitCodeStr = exitCode !== null ? ` [exit=${exitCode}]` : '';
116
+ const errorStr = !success && errorMessage ? ` - ${errorMessage}` : '';
117
+
118
+ toolExecutions.push(`${status} ${toolName}${targetInfo}${exitCodeStr}${errorStr}`);
80
119
  }
81
120
  }
121
+
122
+ // 마지막 assistant 메시지 추출
123
+ let lastAssistantMessage = '';
124
+ for (let i = sourceConversation.length - 1; i >= 0; i--) {
125
+ const entry = sourceConversation[i];
126
+ if (entry?.type === 'message' && entry?.role === 'assistant') {
127
+ const textContent = entry.content?.find(c => c.type === 'output_text');
128
+ if (textContent?.text) {
129
+ lastAssistantMessage = textContent.text;
130
+ break;
131
+ }
132
+ }
133
+ }
134
+
135
+ const toolExecutionSummary = toolExecutions.length > 0
136
+ ? toolExecutions.join('\n')
137
+ : '(도구 실행 없음)';
138
+
139
+ debugLog(`[extractSummarizedContext] Tool executions: ${toolExecutions.length}`);
140
+ debugLog(`[extractSummarizedContext] Last assistant message length: ${lastAssistantMessage.length}`);
141
+ debugLog(`[extractSummarizedContext] END`);
142
+
143
+ return {
144
+ toolExecutionSummary,
145
+ lastAssistantMessage
146
+ };
82
147
  }
83
148
 
84
- function syncOrchestratorConversation() {
85
- debugLog(`[syncOrchestratorConversation] START`);
86
- const sourceConversation = getOrchestratorConversation();
87
- debugLog(`[syncOrchestratorConversation] Source conversation length: ${sourceConversation?.length || 0}`);
88
- debugLog(`[syncOrchestratorConversation] Last snapshot length: ${lastOrchestratorSnapshotLength}`);
89
- debugLog(`[syncOrchestratorConversation] Current completionJudge conversation length: ${completionJudgeConversation.length}`);
149
+ /**
150
+ * 요약된 컨텍스트를 사용자 메시지로 포맷팅합니다.
151
+ */
152
+ function buildContextMessage(mission, summarizedContext, currentTodos) {
153
+ const parts = [];
90
154
 
91
- if (!Array.isArray(sourceConversation) || !sourceConversation.length) {
92
- debugLog(`[syncOrchestratorConversation] No source conversation to sync`);
93
- return;
94
- }
155
+ parts.push(`## 원래 사용자 요청\n${mission}`);
95
156
 
96
- let syncedCount = 0;
97
- for (let i = lastOrchestratorSnapshotLength; i < sourceConversation.length; i++) {
98
- const entry = sourceConversation[i];
99
- if (!entry) continue;
157
+ parts.push(`\n## 실행된 도구들\n${summarizedContext.toolExecutionSummary}`);
100
158
 
101
- // Completion judge는 자체 system 메시지를 유지하므로, Orchestrator의 system 메시지는 제외
102
- if (entry.role === "system") {
103
- debugLog(`[syncOrchestratorConversation] Skipping system message at index ${i}`);
104
- continue;
159
+ if (summarizedContext.lastAssistantMessage) {
160
+ // 너무 길면 truncate
161
+ const maxLength = 2000;
162
+ let assistantMsg = summarizedContext.lastAssistantMessage;
163
+ if (assistantMsg.length > maxLength) {
164
+ assistantMsg = assistantMsg.substring(0, maxLength) + '\n... (truncated)';
105
165
  }
166
+ parts.push(`\n## 에이전트의 마지막 응답\n${assistantMsg}`);
167
+ }
106
168
 
107
- debugLog(`[syncOrchestratorConversation] Syncing entry ${i}: type=${entry.type}, role=${entry.role}`);
108
- // reasoning과 message를 모두 그대로 복사
109
- // ai_request.js에서 이미 reasoning-message 쌍이 유지되고 있음
110
- completionJudgeConversation.push(cloneMessage(entry));
111
- syncedCount++;
169
+ if (currentTodos && currentTodos.length > 0) {
170
+ const todoList = currentTodos.map((todo, index) => {
171
+ const statusIcon = todo.status === 'completed' ? '✓' :
172
+ todo.status === 'in_progress' ? '→' : '○';
173
+ return `${index + 1}. [${statusIcon}] ${todo.content} (${todo.status})`;
174
+ }).join('\n');
175
+ parts.push(`\n## 현재 TODO 상태\n${todoList}`);
112
176
  }
113
177
 
114
- lastOrchestratorSnapshotLength = sourceConversation.length;
115
- debugLog(`[syncOrchestratorConversation] Synced ${syncedCount} entries, new snapshot length: ${lastOrchestratorSnapshotLength}`);
116
- debugLog(`[syncOrchestratorConversation] CompletionJudge conversation now has ${completionJudgeConversation.length} entries`);
117
- debugLog(`[syncOrchestratorConversation] END`);
178
+ return parts.join('\n');
118
179
  }
119
180
 
120
- // 대화 cleanup 및 trim 함수는 conversation_trimmer 모듈에서 가져옴
121
-
122
- async function dispatchCompletionJudgeRequest(options) {
181
+ /**
182
+ * 독립적인 컨텍스트로 completion judge 요청을 보냅니다.
183
+ * Orchestrator 대화를 복사하지 않고, 요약된 정보만 전달합니다.
184
+ */
185
+ async function dispatchCompletionJudgeRequest(options, summarizedContext) {
123
186
  debugLog(`[dispatchCompletionJudgeRequest] START`);
124
187
 
125
188
  if (!options) {
@@ -127,110 +190,89 @@ async function dispatchCompletionJudgeRequest(options) {
127
190
  throw new Error('Completion judge request options not initialized.');
128
191
  }
129
192
 
130
- debugLog(`[dispatchCompletionJudgeRequest] Options - model: ${options.model}, taskName: ${options.taskName}`);
131
-
132
- let attemptCount = 0;
133
- while (true) {
134
- attemptCount++;
135
- debugLog(`[dispatchCompletionJudgeRequest] Attempt ${attemptCount} - starting cleanup...`);
136
- cleanupOrphanOutputs(completionJudgeConversation);
137
-
138
- debugLog(`[dispatchCompletionJudgeRequest] Conversation has ${completionJudgeConversation.length} entries`);
139
-
140
- // Conversation 구조 분석
141
- const conversationTypes = {};
142
- completionJudgeConversation.forEach(entry => {
143
- const type = entry?.type || entry?.role || 'unknown';
144
- conversationTypes[type] = (conversationTypes[type] || 0) + 1;
145
- });
146
- debugLog(`[dispatchCompletionJudgeRequest] Conversation structure: ${JSON.stringify(conversationTypes)}`);
147
-
148
- // function_call_output 항목들 확인
149
- const functionCallOutputs = completionJudgeConversation.filter(item => item.type === 'function_call_output');
150
- debugLog(`[dispatchCompletionJudgeRequest] Found ${functionCallOutputs.length} function_call_output entries`);
151
-
152
- functionCallOutputs.forEach((item, index) => {
153
- debugLog(`[dispatchCompletionJudgeRequest] function_call_output[${index}] - call_id: ${item.call_id}, output length: ${item.output?.length || 0}, output preview: ${item.output?.substring(0, 200)}`);
154
- });
155
-
156
- const { model, isGpt5Model, taskName } = options;
157
-
158
- const requestPayload = {
159
- model,
160
- input: completionJudgeConversation,
161
- text: {
162
- format: {
163
- type: "json_schema",
164
- name: completionJudgmentSchema.name,
165
- strict: completionJudgmentSchema.strict,
166
- schema: completionJudgmentSchema.schema
167
- }
168
- },
169
- reasoning: {},
170
- tool_choice: "none",
171
- tools: [],
172
- top_p: 1,
173
- store: true
174
- };
193
+ const { model, isGpt5Model, taskName, systemMessage, mission, currentTodos } = options;
194
+ debugLog(`[dispatchCompletionJudgeRequest] Options - model: ${model}, taskName: ${taskName}`);
175
195
 
176
- if (!isGpt5Model) {
177
- requestPayload.temperature = 0;
196
+ // 독립적인 대화 구성 (도구 호출 기록 없음)
197
+ const conversation = [
198
+ // System 메시지
199
+ {
200
+ role: "system",
201
+ content: [{ type: "input_text", text: systemMessage }]
202
+ },
203
+ // User 메시지: 요약된 컨텍스트
204
+ {
205
+ role: "user",
206
+ content: [{
207
+ type: "input_text",
208
+ text: buildContextMessage(mission, summarizedContext, currentTodos)
209
+ }]
178
210
  }
211
+ ];
179
212
 
180
- // 토큰 추정
181
- const conversationJson = JSON.stringify(completionJudgeConversation);
182
- const estimatedTokens = Math.ceil(conversationJson.length / 4);
183
- debugLog(`[dispatchCompletionJudgeRequest] Estimated tokens: ${estimatedTokens} (conversation size: ${conversationJson.length} bytes)`);
184
-
185
- debugLog(`[dispatchCompletionJudgeRequest] Sending API request...`);
186
- try {
187
- const response = await request(taskName, requestPayload);
188
- debugLog(`[dispatchCompletionJudgeRequest] API request successful`);
189
- debugLog(`[dispatchCompletionJudgeRequest] Response output_text length: ${response?.output_text?.length || 0}`);
190
- debugLog(`[dispatchCompletionJudgeRequest] END - success`);
191
- return response;
192
- } catch (error) {
193
- debugLog(`[dispatchCompletionJudgeRequest] API request failed: ${error.message}`);
194
- debugLog(`[dispatchCompletionJudgeRequest] Error name: ${error.name}, type: ${error?.constructor?.name}`);
195
-
196
- // AbortError는 즉시 전파 (세션 중단)
197
- if (error.name === 'AbortError') {
198
- debugLog(`[dispatchCompletionJudgeRequest] Request aborted by user, propagating AbortError`);
199
- throw error;
213
+ const requestPayload = {
214
+ model,
215
+ input: conversation,
216
+ text: {
217
+ format: {
218
+ type: "json_schema",
219
+ name: completionJudgmentSchema.name,
220
+ strict: completionJudgmentSchema.strict,
221
+ schema: completionJudgmentSchema.schema
200
222
  }
223
+ },
224
+ reasoning: {},
225
+ tool_choice: "none",
226
+ tools: [],
227
+ top_p: 1,
228
+ store: true
229
+ };
201
230
 
202
- if (!shouldRetryWithTrim(error)) {
203
- debugLog(`[dispatchCompletionJudgeRequest] Not recoverable by trimming, re-throwing`);
204
- throw error;
205
- }
231
+ if (!isGpt5Model) {
232
+ requestPayload.temperature = 0;
233
+ }
206
234
 
207
- debugLog(`[dispatchCompletionJudgeRequest] Recoverable error detected, attempting to trim...`);
208
- const trimmed = trimConversation(completionJudgeConversation);
209
- debugLog(`[dispatchCompletionJudgeRequest] Trim result: ${trimmed}, new conversation length: ${completionJudgeConversation.length}`);
210
- if (!trimmed) {
211
- debugLog(`[dispatchCompletionJudgeRequest] Cannot trim further, re-throwing error`);
212
- throw error;
213
- }
214
- debugLog(`[dispatchCompletionJudgeRequest] Retrying with trimmed conversation...`);
235
+ // 토큰 추정
236
+ const conversationJson = JSON.stringify(conversation);
237
+ const estimatedTokens = Math.ceil(conversationJson.length / 4);
238
+ debugLog(`[dispatchCompletionJudgeRequest] Estimated tokens: ${estimatedTokens} (conversation size: ${conversationJson.length} bytes)`);
239
+
240
+ const apiStartTime = Date.now();
241
+ debugLog(`[dispatchCompletionJudgeRequest] >>>>> Sending API request at ${new Date(apiStartTime).toISOString()}...`);
242
+
243
+ try {
244
+ const response = await request(taskName, requestPayload);
245
+ const apiDuration = Date.now() - apiStartTime;
246
+ debugLog(`[dispatchCompletionJudgeRequest] <<<<< API request successful in ${apiDuration}ms`);
247
+ debugLog(`[dispatchCompletionJudgeRequest] Response output_text length: ${response?.output_text?.length || 0}`);
248
+ debugLog(`[dispatchCompletionJudgeRequest] END - success`);
249
+ return response;
250
+ } catch (error) {
251
+ const apiDuration = Date.now() - apiStartTime;
252
+ debugLog(`[dispatchCompletionJudgeRequest] <<<<< API request FAILED after ${apiDuration}ms: ${error.message}`);
253
+ debugLog(`[dispatchCompletionJudgeRequest] Error name: ${error.name}, type: ${error?.constructor?.name}`);
254
+
255
+ // AbortError는 즉시 전파 (세션 중단)
256
+ if (error.name === 'AbortError') {
257
+ debugLog(`[dispatchCompletionJudgeRequest] Request aborted by user, propagating AbortError`);
258
+ throw error;
215
259
  }
260
+
261
+ throw error;
216
262
  }
217
263
  }
218
264
 
265
+ // Legacy exports (하위 호환성 - 실제로는 더 이상 사용하지 않음)
219
266
  export function resetCompletionJudgeConversation() {
220
- completionJudgeConversation.length = 0;
221
- lastOrchestratorSnapshotLength = 0;
267
+ // No-op: 독립 컨텍스트 방식에서는 대화를 유지하지 않음
222
268
  }
223
269
 
224
270
  export function getCompletionJudgeConversation() {
225
- return completionJudgeConversation;
271
+ return [];
226
272
  }
227
273
 
228
274
  export function restoreCompletionJudgeConversation(savedConversation, savedSnapshotLength = 0) {
229
- completionJudgeConversation.length = 0;
230
- if (Array.isArray(savedConversation)) {
231
- completionJudgeConversation.push(...savedConversation);
232
- }
233
- lastOrchestratorSnapshotLength = savedSnapshotLength;
275
+ // No-op: 독립 컨텍스트 방식에서는 복원할 대화가 없음
234
276
  }
235
277
 
236
278
  /**
@@ -246,49 +288,77 @@ export async function judgeMissionCompletion(templateVars = {}) {
246
288
  debugLog('=== judgeMissionCompletion START =======');
247
289
  debugLog('========================================');
248
290
  debugLog(`[judgeMissionCompletion] Called with templateVars: ${JSON.stringify(Object.keys(templateVars))}`);
249
- if (templateVars.what_user_requests) {
250
- debugLog(`[judgeMissionCompletion] what_user_requests: ${templateVars.what_user_requests.substring(0, 200)}`);
291
+
292
+ const mission = templateVars.what_user_requests || '';
293
+ if (mission) {
294
+ debugLog(`[judgeMissionCompletion] what_user_requests: ${mission.substring(0, 200)}`);
251
295
  }
252
296
 
253
297
  try {
254
298
  debugLog(`[judgeMissionCompletion] Creating request options...`);
255
- const requestOptions = await createCompletionJudgeRequestOptions();
256
- debugLog(`[judgeMissionCompletion] Request options - model: ${requestOptions.model}, isGpt5Model: ${requestOptions.isGpt5Model}`);
299
+ const baseOptions = await createCompletionJudgeRequestOptions();
300
+ debugLog(`[judgeMissionCompletion] Request options - model: ${baseOptions.model}, isGpt5Model: ${baseOptions.isGpt5Model}`);
257
301
 
258
- // 현재 todos를 가져와서 templateVars에 추가
302
+ // 현재 todos를 가져옴
259
303
  const currentTodos = getCurrentTodos();
260
304
  debugLog(`[judgeMissionCompletion] Current todos count: ${currentTodos.length}`);
261
305
 
262
- // templateVars에 currentTodos 추가 (EJS 템플릿에서 조건부 렌더링)
306
+ // 시스템 메시지 로드
263
307
  const enhancedTemplateVars = {
264
308
  ...templateVars,
265
309
  currentTodos: currentTodos
266
310
  };
311
+ const systemMessageObj = await createSystemMessage("completion_judge.txt", enhancedTemplateVars);
312
+ debugLog(`[judgeMissionCompletion] System message loaded`);
313
+
314
+ // Orchestrator 대화에서 요약된 컨텍스트 추출
315
+ debugLog(`[judgeMissionCompletion] Extracting summarized context...`);
316
+ const summarizedContext = extractSummarizedContext();
317
+ debugLog(`[judgeMissionCompletion] Context extracted - tools: ${summarizedContext.toolExecutionSummary.split('\n').length}, assistant msg len: ${summarizedContext.lastAssistantMessage.length}`);
318
+
319
+ // 옵션에 추가 정보 포함
320
+ const requestOptions = {
321
+ ...baseOptions,
322
+ systemMessage: systemMessageObj.content,
323
+ mission: mission,
324
+ currentTodos: currentTodos
325
+ };
267
326
 
268
- // Completion judge 자체 system 메시지 초기화 (템플릿 변수 전달)
269
- debugLog(`[judgeMissionCompletion] Initializing completion judge conversation...`);
270
- await ensureCompletionJudgeConversationInitialized(enhancedTemplateVars);
271
- debugLog(`[judgeMissionCompletion] Conversation initialized, length: ${completionJudgeConversation.length}`);
272
-
273
- // Orchestrator의 대화 내용 동기화 (system 메시지 제외)
274
- debugLog(`[judgeMissionCompletion] Syncing orchestrator conversation...`);
275
- syncOrchestratorConversation();
276
- debugLog(`[judgeMissionCompletion] Sync complete, conversation length: ${completionJudgeConversation.length}`);
277
-
278
- debugLog(`[judgeMissionCompletion] Sending request with ${completionJudgeConversation.length} conversation entries`);
279
-
280
- const response = await dispatchCompletionJudgeRequest(requestOptions);
327
+ debugLog(`[judgeMissionCompletion] Sending request with summarized context...`);
328
+ const response = await dispatchCompletionJudgeRequest(requestOptions, summarizedContext);
281
329
 
282
330
  debugLog(`[judgeMissionCompletion] Received response, parsing output_text`);
283
331
  debugLog(`[judgeMissionCompletion] Raw output_text: ${response.output_text?.substring(0, 500)}`);
284
332
 
285
- // Completion judge 자신의 응답은 히스토리에 추가하지 않음 (Orchestrator와 동일한 히스토리 유지)
286
- // 대화 기록 초기화 (다음 판단을 위해)
287
- debugLog(`[judgeMissionCompletion] Resetting completion judge conversation...`);
288
- resetCompletionJudgeConversation();
289
-
290
333
  try {
291
- const judgment = JSON.parse(response.output_text);
334
+ let outputText = response.output_text || '';
335
+ let judgment = null;
336
+
337
+ // 1차 시도: 전체 텍스트를 JSON으로 파싱
338
+ try {
339
+ judgment = JSON.parse(outputText);
340
+ debugLog(`[judgeMissionCompletion] Parsed as pure JSON`);
341
+ } catch (e) {
342
+ // 2차 시도: 텍스트 끝에 붙은 JSON 추출 (GLM 모델 대응)
343
+ // 패턴: ...텍스트...{"should_complete":true,"whatUserShouldSay":"..."}
344
+ const jsonMatch = outputText.match(/\{[^{}]*"should_complete"\s*:\s*(true|false)[^{}]*\}/);
345
+ if (jsonMatch) {
346
+ debugLog(`[judgeMissionCompletion] Extracted JSON from text: ${jsonMatch[0]}`);
347
+ judgment = JSON.parse(jsonMatch[0]);
348
+ } else {
349
+ // 3차 시도: 마지막 { } 블록 추출
350
+ const lastBraceStart = outputText.lastIndexOf('{');
351
+ const lastBraceEnd = outputText.lastIndexOf('}');
352
+ if (lastBraceStart !== -1 && lastBraceEnd > lastBraceStart) {
353
+ const jsonCandidate = outputText.substring(lastBraceStart, lastBraceEnd + 1);
354
+ debugLog(`[judgeMissionCompletion] Trying last JSON block: ${jsonCandidate}`);
355
+ judgment = JSON.parse(jsonCandidate);
356
+ } else {
357
+ throw new Error('No JSON found in output');
358
+ }
359
+ }
360
+ }
361
+
292
362
  debugLog(`[judgeMissionCompletion] Parsed judgment successfully`);
293
363
  debugLog(`[judgeMissionCompletion] should_complete: ${judgment.should_complete}`);
294
364
  debugLog(`[judgeMissionCompletion] whatUserShouldSay: ${judgment.whatUserShouldSay}`);
@@ -308,17 +378,24 @@ export async function judgeMissionCompletion(templateVars = {}) {
308
378
  } catch (parseError) {
309
379
  debugLog(`[judgeMissionCompletion] Parse error: ${parseError.message}`);
310
380
  debugLog(`[judgeMissionCompletion] Failed to parse output_text as JSON`);
311
- throw new Error('Completion judge response did not include valid JSON output_text.');
381
+ debugLog(`[judgeMissionCompletion] Raw output_text: ${response.output_text}`);
382
+
383
+ // JSON 파싱 실패 시 안전하게 완료 처리 (무한 루프 방지)
384
+ // orchestrator가 이미 message만 반환한 시점이므로 완료로 처리
385
+ debugLog(`[judgeMissionCompletion] Defaulting to complete to prevent infinite loop`);
386
+
387
+ debugLog('========================================');
388
+ debugLog('=== judgeMissionCompletion END (PARSE_ERROR) =');
389
+ debugLog('========================================');
390
+ debugLog('');
391
+
392
+ return { shouldComplete: true, whatUserShouldSay: "" };
312
393
  }
313
394
 
314
395
  } catch (error) {
315
396
  debugLog(`[judgeMissionCompletion] ERROR: ${error.message}, error.name: ${error.name}`);
316
397
  debugLog(`[judgeMissionCompletion] Error stack: ${error.stack}`);
317
398
 
318
- // 에러 발생 시 대화 기록 초기화
319
- debugLog(`[judgeMissionCompletion] Resetting completion judge conversation due to error...`);
320
- resetCompletionJudgeConversation();
321
-
322
399
  // AbortError는 즉시 전파 (세션 중단)
323
400
  if (error.name === 'AbortError') {
324
401
  debugLog(`[judgeMissionCompletion] AbortError detected, propagating to caller`);
@@ -329,15 +406,15 @@ export async function judgeMissionCompletion(templateVars = {}) {
329
406
  throw error;
330
407
  }
331
408
 
332
- // 다른 에러 발생 시 안전하게 계속 진행
333
- debugLog(`[judgeMissionCompletion] Non-abort error, returning safe default (shouldComplete=false)`);
409
+ // 다른 에러 발생 시 안전하게 완료 처리 (무한 루프 방지)
410
+ debugLog(`[judgeMissionCompletion] Non-abort error, returning safe default (shouldComplete=true to prevent loop)`);
334
411
  debugLog('========================================');
335
412
  debugLog('=== judgeMissionCompletion END (ERROR) =');
336
413
  debugLog('========================================');
337
414
  debugLog('');
338
415
 
339
416
  return {
340
- shouldComplete: false,
417
+ shouldComplete: true,
341
418
  whatUserShouldSay: ""
342
419
  };
343
420
  }