triflux 3.3.0-dev.1 → 3.3.0-dev.5
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/bin/triflux.mjs +169 -39
- package/hooks/hooks.json +5 -0
- package/hub/assign-callbacks.mjs +136 -0
- package/hub/bridge.mjs +283 -97
- package/hub/pipe.mjs +104 -0
- package/hub/router.mjs +322 -1
- package/hub/schema.sql +40 -7
- package/hub/server.mjs +151 -53
- package/hub/store.mjs +293 -1
- package/hub/team/cli-team-status.mjs +17 -3
- package/hub/team/native-supervisor.mjs +62 -22
- package/hub/team/native.mjs +86 -10
- package/hub/team/psmux.mjs +555 -115
- package/hub/tools.mjs +101 -26
- package/hub/workers/delegator-mcp.mjs +1045 -0
- package/hub/workers/factory.mjs +3 -0
- package/hub/workers/interface.mjs +2 -2
- package/hud/hud-qos-status.mjs +1735 -1790
- package/package.json +60 -60
- package/scripts/__tests__/keyword-detector.test.mjs +3 -3
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/hub-ensure.mjs +21 -3
- package/scripts/lib/mcp-filter.mjs +637 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/mcp-check.mjs +126 -88
- package/scripts/setup.mjs +15 -10
- package/scripts/test-tfx-route-no-claude-native.mjs +10 -2
- package/scripts/tfx-route.sh +434 -179
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hub/workers/delegator-mcp.mjs — triflux 위임용 MCP stdio 서버
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import * as z from 'zod';
|
|
14
|
+
|
|
15
|
+
import { CodexMcpWorker } from './codex-mcp.mjs';
|
|
16
|
+
import { GeminiWorker } from './gemini-worker.mjs';
|
|
17
|
+
import {
|
|
18
|
+
buildPromptHint,
|
|
19
|
+
getCodexMcpConfig,
|
|
20
|
+
getGeminiAllowedServers,
|
|
21
|
+
resolveMcpProfile,
|
|
22
|
+
SUPPORTED_MCP_PROFILES,
|
|
23
|
+
} from '../../scripts/lib/mcp-filter.mjs';
|
|
24
|
+
|
|
25
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const SERVER_INFO = { name: 'triflux-delegator', version: '1.0.0' };
|
|
27
|
+
const DEFAULT_CONTEXT_BYTES = 32 * 1024;
|
|
28
|
+
const DEFAULT_ROUTE_TIMEOUT_SEC = 120;
|
|
29
|
+
const DIRECT_PROGRESS_START = 5;
|
|
30
|
+
const DIRECT_PROGRESS_RUNNING = 60;
|
|
31
|
+
const DIRECT_PROGRESS_DONE = 100;
|
|
32
|
+
|
|
33
|
+
const AGENT_TIMEOUT_SEC = Object.freeze({
|
|
34
|
+
executor: 1080,
|
|
35
|
+
'build-fixer': 540,
|
|
36
|
+
debugger: 900,
|
|
37
|
+
'deep-executor': 3600,
|
|
38
|
+
architect: 3600,
|
|
39
|
+
planner: 3600,
|
|
40
|
+
critic: 3600,
|
|
41
|
+
analyst: 3600,
|
|
42
|
+
'code-reviewer': 1800,
|
|
43
|
+
'security-reviewer': 1800,
|
|
44
|
+
'quality-reviewer': 1800,
|
|
45
|
+
scientist: 1440,
|
|
46
|
+
'scientist-deep': 3600,
|
|
47
|
+
'document-specialist': 1440,
|
|
48
|
+
designer: 900,
|
|
49
|
+
writer: 900,
|
|
50
|
+
explore: 300,
|
|
51
|
+
verifier: 1200,
|
|
52
|
+
'test-engineer': 300,
|
|
53
|
+
'qa-tester': 300,
|
|
54
|
+
spark: 180,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const CODEX_PROFILE_BY_AGENT = Object.freeze({
|
|
58
|
+
executor: 'high',
|
|
59
|
+
'build-fixer': 'fast',
|
|
60
|
+
debugger: 'high',
|
|
61
|
+
'deep-executor': 'xhigh',
|
|
62
|
+
architect: 'xhigh',
|
|
63
|
+
planner: 'xhigh',
|
|
64
|
+
critic: 'xhigh',
|
|
65
|
+
analyst: 'xhigh',
|
|
66
|
+
'code-reviewer': 'thorough',
|
|
67
|
+
'security-reviewer': 'thorough',
|
|
68
|
+
'quality-reviewer': 'thorough',
|
|
69
|
+
scientist: 'high',
|
|
70
|
+
'scientist-deep': 'thorough',
|
|
71
|
+
'document-specialist': 'high',
|
|
72
|
+
verifier: 'thorough',
|
|
73
|
+
spark: 'spark_fast',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const GEMINI_MODEL_BY_AGENT = Object.freeze({
|
|
77
|
+
'build-fixer': 'gemini-3-flash-preview',
|
|
78
|
+
writer: 'gemini-3-flash-preview',
|
|
79
|
+
spark: 'gemini-3-flash-preview',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const REVIEW_INSTRUCTION_BY_AGENT = Object.freeze({
|
|
83
|
+
'code-reviewer': '코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라.',
|
|
84
|
+
'security-reviewer': '보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라.',
|
|
85
|
+
'quality-reviewer': '품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라.',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function cloneEnv(env = process.env) {
|
|
89
|
+
return Object.fromEntries(
|
|
90
|
+
Object.entries(env).filter(([, value]) => typeof value === 'string'),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseJsonArray(raw, fallback = []) {
|
|
95
|
+
if (!raw) return [...fallback];
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
return Array.isArray(parsed)
|
|
99
|
+
? parsed.map((item) => String(item ?? '')).filter(Boolean)
|
|
100
|
+
: [...fallback];
|
|
101
|
+
} catch {
|
|
102
|
+
return [...fallback];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveCandidatePath(candidate, cwd = process.cwd()) {
|
|
107
|
+
if (!candidate) return null;
|
|
108
|
+
const normalized = isAbsolute(candidate) ? candidate : resolve(cwd, candidate);
|
|
109
|
+
return existsSync(normalized) ? normalized : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveRouteScript(explicitPath, cwd = process.cwd()) {
|
|
113
|
+
const candidates = [
|
|
114
|
+
explicitPath,
|
|
115
|
+
process.env.TFX_DELEGATOR_ROUTE_SCRIPT,
|
|
116
|
+
process.env.TFX_ROUTE_SCRIPT,
|
|
117
|
+
resolve(SCRIPT_DIR, '..', '..', 'scripts', 'tfx-route.sh'),
|
|
118
|
+
resolve(cwd, 'scripts', 'tfx-route.sh'),
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const candidate of candidates) {
|
|
122
|
+
const resolved = resolveCandidatePath(candidate, cwd);
|
|
123
|
+
if (resolved) return resolved;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveCodexProfile(agentType) {
|
|
130
|
+
return CODEX_PROFILE_BY_AGENT[agentType] || 'high';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveGeminiModel(agentType) {
|
|
134
|
+
return GEMINI_MODEL_BY_AGENT[agentType] || 'gemini-3.1-pro-preview';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveTimeoutMs(agentType, timeoutMs) {
|
|
138
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
139
|
+
return Math.trunc(timeoutMs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const timeoutSec = AGENT_TIMEOUT_SEC[agentType] || DEFAULT_ROUTE_TIMEOUT_SEC;
|
|
143
|
+
return timeoutSec * 1000;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveTimeoutSec(agentType, timeoutMs) {
|
|
147
|
+
const resolved = resolveTimeoutMs(agentType, timeoutMs);
|
|
148
|
+
return Math.max(1, Math.ceil(resolved / 1000));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function loadContextFromFile(contextFile) {
|
|
152
|
+
if (!contextFile) return '';
|
|
153
|
+
const resolved = resolveCandidatePath(contextFile);
|
|
154
|
+
if (!resolved) return '';
|
|
155
|
+
try {
|
|
156
|
+
return readFileSync(resolved, 'utf8').slice(0, DEFAULT_CONTEXT_BYTES);
|
|
157
|
+
} catch {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function withContext(prompt, contextFile) {
|
|
163
|
+
const context = loadContextFromFile(contextFile);
|
|
164
|
+
if (!context) return prompt;
|
|
165
|
+
return `${prompt}\n\n<prior_context>\n${context}\n</prior_context>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function withPromptHint(prompt, args) {
|
|
169
|
+
const promptWithContext = withContext(prompt, args.contextFile);
|
|
170
|
+
const hint = buildPromptHint({
|
|
171
|
+
agentType: args.agentType,
|
|
172
|
+
requestedProfile: args.mcpProfile,
|
|
173
|
+
searchTool: args.searchTool,
|
|
174
|
+
workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
|
|
175
|
+
taskText: promptWithContext,
|
|
176
|
+
});
|
|
177
|
+
if (!hint) return promptWithContext;
|
|
178
|
+
return `${promptWithContext}. ${hint}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function joinInstructions(...values) {
|
|
182
|
+
return values
|
|
183
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
184
|
+
.join('\n')
|
|
185
|
+
.trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseRouteType(stderr = '') {
|
|
189
|
+
const match = stderr.match(/type=([a-z-]+)/);
|
|
190
|
+
if (!match) return null;
|
|
191
|
+
if (match[1] === 'codex') return 'codex';
|
|
192
|
+
if (match[1] === 'gemini') return 'gemini';
|
|
193
|
+
return match[1];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function summarizePayload(payload) {
|
|
197
|
+
if (typeof payload.output === 'string' && payload.output.trim()) return payload.output.trim();
|
|
198
|
+
if (payload.mode === 'async' && payload.jobId) return `비동기 위임이 시작되었습니다. jobId=${payload.jobId}`;
|
|
199
|
+
if (payload.jobId) return `jobId=${payload.jobId} 상태=${payload.status}`;
|
|
200
|
+
if (payload.status) return `상태=${payload.status}`;
|
|
201
|
+
return payload.ok ? '완료되었습니다.' : '실패했습니다.';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createToolResponse(payload, { isError = false } = {}) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: 'text', text: summarizePayload(payload) }],
|
|
207
|
+
structuredContent: payload,
|
|
208
|
+
isError,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createErrorPayload(message, extras = {}) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
status: 'failed',
|
|
216
|
+
error: message,
|
|
217
|
+
...extras,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const DelegateInputSchema = z.object({
|
|
222
|
+
prompt: z.string().min(1).describe('위임할 프롬프트'),
|
|
223
|
+
provider: z.enum(['auto', 'codex', 'gemini']).default('auto').describe('사용할 provider'),
|
|
224
|
+
mode: z.enum(['sync', 'async']).default('sync').describe('동기 또는 비동기 실행'),
|
|
225
|
+
agentType: z.string().default('executor').describe('tfx-route 역할명 또는 direct 실행 역할'),
|
|
226
|
+
cwd: z.string().optional().describe('작업 디렉터리'),
|
|
227
|
+
timeoutMs: z.number().int().positive().optional().describe('요청 타임아웃(ms)'),
|
|
228
|
+
sessionKey: z.string().optional().describe('Codex warm session 재사용 키'),
|
|
229
|
+
resetSession: z.boolean().optional().describe('기존 Codex 세션 초기화 여부'),
|
|
230
|
+
mcpProfile: z.enum(SUPPORTED_MCP_PROFILES).default('auto'),
|
|
231
|
+
contextFile: z.string().optional().describe('tfx-route prior_context 파일 경로'),
|
|
232
|
+
searchTool: z.enum(['brave-search', 'tavily', 'exa']).optional().describe('검색 우선 도구'),
|
|
233
|
+
workerIndex: z.number().int().positive().optional().describe('병렬 워커 인덱스'),
|
|
234
|
+
model: z.string().optional().describe('직접 실행 시 모델 오버라이드'),
|
|
235
|
+
developerInstructions: z.string().optional().describe('직접 실행 시 추가 개발자 지침'),
|
|
236
|
+
compactPrompt: z.string().optional().describe('Codex compact prompt'),
|
|
237
|
+
threadId: z.string().optional().describe('Codex 직접 실행 시 기존 threadId'),
|
|
238
|
+
codexTransport: z.enum(['auto', 'mcp', 'exec']).optional().describe('route 경로용 Codex transport'),
|
|
239
|
+
noClaudeNative: z.boolean().optional().describe('route 경로용 TFX_NO_CLAUDE_NATIVE'),
|
|
240
|
+
teamName: z.string().optional().describe('TFX_TEAM_NAME'),
|
|
241
|
+
teamTaskId: z.string().optional().describe('TFX_TEAM_TASK_ID'),
|
|
242
|
+
teamAgentName: z.string().optional().describe('TFX_TEAM_AGENT_NAME'),
|
|
243
|
+
teamLeadName: z.string().optional().describe('TFX_TEAM_LEAD_NAME'),
|
|
244
|
+
hubUrl: z.string().optional().describe('TFX_HUB_URL'),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const DelegateStatusInputSchema = z.object({
|
|
248
|
+
jobId: z.string().min(1).describe('조회할 비동기 job ID'),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const DelegateReplyInputSchema = z.object({
|
|
252
|
+
job_id: z.string().min(1).describe('후속 응답을 보낼 기존 delegate job ID'),
|
|
253
|
+
reply: z.string().min(1).describe('후속 사용자 응답'),
|
|
254
|
+
done: z.boolean().default(false).describe('true이면 응답 처리 후 대화를 종료'),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const DelegateOutputSchema = z.object({
|
|
258
|
+
ok: z.boolean(),
|
|
259
|
+
jobId: z.string().optional(),
|
|
260
|
+
job_id: z.string().optional(),
|
|
261
|
+
mode: z.enum(['sync', 'async']).optional(),
|
|
262
|
+
status: z.enum(['running', 'completed', 'failed']).optional(),
|
|
263
|
+
error: z.string().optional(),
|
|
264
|
+
providerRequested: z.string().optional(),
|
|
265
|
+
providerResolved: z.string().nullable().optional(),
|
|
266
|
+
agentType: z.string().optional(),
|
|
267
|
+
transport: z.string().optional(),
|
|
268
|
+
createdAt: z.string().optional(),
|
|
269
|
+
startedAt: z.string().optional(),
|
|
270
|
+
updatedAt: z.string().optional(),
|
|
271
|
+
completedAt: z.string().nullable().optional(),
|
|
272
|
+
exitCode: z.number().nullable().optional(),
|
|
273
|
+
output: z.string().optional(),
|
|
274
|
+
stderr: z.string().optional(),
|
|
275
|
+
threadId: z.string().nullable().optional(),
|
|
276
|
+
sessionKey: z.string().nullable().optional(),
|
|
277
|
+
conversationOpen: z.boolean().optional(),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
function isTeamRouteRequested(args) {
|
|
281
|
+
return Boolean(
|
|
282
|
+
args.teamName
|
|
283
|
+
|| args.teamTaskId
|
|
284
|
+
|| args.teamAgentName
|
|
285
|
+
|| args.teamLeadName
|
|
286
|
+
|| args.hubUrl
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function pickRouteMode(provider) {
|
|
291
|
+
return provider === 'auto' ? 'auto' : provider;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function sanitizeDelegateArgs(args = {}) {
|
|
295
|
+
return {
|
|
296
|
+
provider: args.provider || 'auto',
|
|
297
|
+
agentType: args.agentType || 'executor',
|
|
298
|
+
cwd: args.cwd || null,
|
|
299
|
+
timeoutMs: Number.isFinite(Number(args.timeoutMs)) ? Math.trunc(Number(args.timeoutMs)) : null,
|
|
300
|
+
sessionKey: args.sessionKey || null,
|
|
301
|
+
resetSession: Boolean(args.resetSession),
|
|
302
|
+
mcpProfile: args.mcpProfile || 'auto',
|
|
303
|
+
contextFile: args.contextFile || null,
|
|
304
|
+
searchTool: args.searchTool || null,
|
|
305
|
+
workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : null,
|
|
306
|
+
model: args.model || null,
|
|
307
|
+
developerInstructions: args.developerInstructions || null,
|
|
308
|
+
compactPrompt: args.compactPrompt || null,
|
|
309
|
+
threadId: args.threadId || null,
|
|
310
|
+
codexTransport: args.codexTransport || null,
|
|
311
|
+
noClaudeNative: args.noClaudeNative === true,
|
|
312
|
+
teamName: args.teamName || null,
|
|
313
|
+
teamTaskId: args.teamTaskId || null,
|
|
314
|
+
teamAgentName: args.teamAgentName || null,
|
|
315
|
+
teamLeadName: args.teamLeadName || null,
|
|
316
|
+
hubUrl: args.hubUrl || null,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatConversationTranscript(turns = []) {
|
|
321
|
+
return turns.map((turn, index) => {
|
|
322
|
+
const parts = [
|
|
323
|
+
`Turn ${index + 1} user:\n${turn.user}`,
|
|
324
|
+
];
|
|
325
|
+
if (typeof turn.assistant === 'string' && turn.assistant.trim()) {
|
|
326
|
+
parts.push(`Turn ${index + 1} assistant:\n${turn.assistant}`);
|
|
327
|
+
}
|
|
328
|
+
return parts.join('\n\n');
|
|
329
|
+
}).join('\n\n');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function emitProgress(extra, progress, total, message) {
|
|
333
|
+
if (extra?._meta?.progressToken === undefined) return;
|
|
334
|
+
await extra.sendNotification({
|
|
335
|
+
method: 'notifications/progress',
|
|
336
|
+
params: {
|
|
337
|
+
progressToken: extra._meta.progressToken,
|
|
338
|
+
progress,
|
|
339
|
+
total,
|
|
340
|
+
message,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export class DelegatorMcpWorker {
|
|
346
|
+
type = 'delegator';
|
|
347
|
+
|
|
348
|
+
constructor(options = {}) {
|
|
349
|
+
this.cwd = options.cwd || process.cwd();
|
|
350
|
+
this.env = cloneEnv({ ...cloneEnv(process.env), ...cloneEnv(options.env) });
|
|
351
|
+
this.routeScript = resolveRouteScript(options.routeScript, this.cwd);
|
|
352
|
+
this.bashCommand = options.bashCommand
|
|
353
|
+
|| this.env.TFX_DELEGATOR_BASH_COMMAND
|
|
354
|
+
|| this.env.BASH_BIN
|
|
355
|
+
|| 'bash';
|
|
356
|
+
|
|
357
|
+
this.codexWorker = new CodexMcpWorker({
|
|
358
|
+
command: options.codexCommand
|
|
359
|
+
|| this.env.TFX_DELEGATOR_CODEX_COMMAND
|
|
360
|
+
|| this.env.CODEX_BIN
|
|
361
|
+
|| 'codex',
|
|
362
|
+
args: Array.isArray(options.codexArgs) && options.codexArgs.length
|
|
363
|
+
? options.codexArgs
|
|
364
|
+
: parseJsonArray(this.env.TFX_DELEGATOR_CODEX_ARGS_JSON, []),
|
|
365
|
+
cwd: this.cwd,
|
|
366
|
+
env: this.env,
|
|
367
|
+
clientInfo: { name: SERVER_INFO.name, version: SERVER_INFO.version },
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
this.geminiCommand = options.geminiCommand || this.env.GEMINI_BIN || 'gemini';
|
|
371
|
+
this.geminiCommandArgs = Array.isArray(options.geminiArgs) && options.geminiArgs.length
|
|
372
|
+
? [...options.geminiArgs]
|
|
373
|
+
: parseJsonArray(this.env.GEMINI_BIN_ARGS_JSON, []);
|
|
374
|
+
|
|
375
|
+
this.server = null;
|
|
376
|
+
this.transport = null;
|
|
377
|
+
this.jobs = new Map();
|
|
378
|
+
this.geminiConversations = new Map();
|
|
379
|
+
this.routeChildren = new Set();
|
|
380
|
+
this.ready = false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
isReady() {
|
|
384
|
+
return this.ready;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async start() {
|
|
388
|
+
if (this.server) {
|
|
389
|
+
this.ready = true;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const server = new McpServer(SERVER_INFO, {
|
|
394
|
+
capabilities: { logging: {} },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
server.registerTool('triflux-delegate', {
|
|
398
|
+
description: '새 위임을 실행합니다. codex/gemini direct 경로와 tfx-route 기반 auto 라우팅을 모두 지원합니다.',
|
|
399
|
+
inputSchema: DelegateInputSchema,
|
|
400
|
+
outputSchema: DelegateOutputSchema,
|
|
401
|
+
}, async (args, extra) => {
|
|
402
|
+
const payload = await this.delegate(args, extra);
|
|
403
|
+
return createToolResponse(payload, { isError: payload.ok === false && payload.mode !== 'async' });
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
server.registerTool('triflux-delegate-status', {
|
|
407
|
+
description: '비동기 위임 job 상태를 조회합니다.',
|
|
408
|
+
inputSchema: DelegateStatusInputSchema,
|
|
409
|
+
outputSchema: DelegateOutputSchema,
|
|
410
|
+
}, async ({ jobId }, extra) => {
|
|
411
|
+
const payload = await this.getJobStatus(jobId, extra);
|
|
412
|
+
return createToolResponse(payload, { isError: payload.ok === false });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
server.registerTool('triflux-delegate-reply', {
|
|
416
|
+
description: '기존 delegate job에 후속 응답을 보내고, Gemini direct job이면 multi-turn 대화를 이어갑니다.',
|
|
417
|
+
inputSchema: DelegateReplyInputSchema,
|
|
418
|
+
outputSchema: DelegateOutputSchema,
|
|
419
|
+
}, async (args, extra) => {
|
|
420
|
+
const payload = await this.reply(args, extra);
|
|
421
|
+
return createToolResponse(payload, { isError: payload.ok === false });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
this.server = server;
|
|
425
|
+
this.ready = true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async serveStdio() {
|
|
429
|
+
await this.start();
|
|
430
|
+
if (this.transport) return;
|
|
431
|
+
const transport = new StdioServerTransport();
|
|
432
|
+
await this.server.connect(transport);
|
|
433
|
+
this.transport = transport;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async stop() {
|
|
437
|
+
this.ready = false;
|
|
438
|
+
|
|
439
|
+
for (const child of this.routeChildren) {
|
|
440
|
+
try { child.kill(); } catch {}
|
|
441
|
+
}
|
|
442
|
+
this.routeChildren.clear();
|
|
443
|
+
|
|
444
|
+
await this.codexWorker.stop().catch(() => {});
|
|
445
|
+
|
|
446
|
+
for (const job of this.jobs.values()) {
|
|
447
|
+
if (job.worker) {
|
|
448
|
+
await job.worker.stop().catch(() => {});
|
|
449
|
+
job.worker = null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
this.geminiConversations.clear();
|
|
453
|
+
|
|
454
|
+
if (this.server) {
|
|
455
|
+
await this.server.close().catch(() => {});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.server = null;
|
|
459
|
+
this.transport = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async run(prompt, options = {}) {
|
|
463
|
+
return this._executeDirect({ prompt, ...options });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async execute(prompt, options = {}) {
|
|
467
|
+
const result = await this._executeDirect({ prompt, ...options });
|
|
468
|
+
return {
|
|
469
|
+
output: result.output || result.error || '',
|
|
470
|
+
exitCode: result.exitCode ?? (result.ok ? 0 : 1),
|
|
471
|
+
threadId: result.threadId || null,
|
|
472
|
+
sessionKey: result.sessionKey || null,
|
|
473
|
+
raw: result,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async delegate(args, extra) {
|
|
478
|
+
if (args.mode === 'async') {
|
|
479
|
+
return this._startAsyncJob(args, extra);
|
|
480
|
+
}
|
|
481
|
+
return this._runSyncJob(args, extra);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async getJobStatus(jobId, extra) {
|
|
485
|
+
const job = this.jobs.get(jobId);
|
|
486
|
+
if (!job) {
|
|
487
|
+
return createErrorPayload(`알 수 없는 jobId: ${jobId}`, { jobId });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const payload = this._serializeJob(job);
|
|
491
|
+
if (job.status === 'running') {
|
|
492
|
+
await emitProgress(extra, 25, 100, `job ${jobId} 실행 중`);
|
|
493
|
+
} else if (job.status === 'completed') {
|
|
494
|
+
await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${jobId} 완료`);
|
|
495
|
+
} else if (job.status === 'failed') {
|
|
496
|
+
await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${jobId} 실패`);
|
|
497
|
+
}
|
|
498
|
+
return payload;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async reply({ job_id, reply, done = false }, extra) {
|
|
502
|
+
const job = this.jobs.get(job_id);
|
|
503
|
+
if (!job) {
|
|
504
|
+
return createErrorPayload(`알 수 없는 jobId: ${job_id}`, { jobId: job_id, job_id });
|
|
505
|
+
}
|
|
506
|
+
if (job.status === 'running') {
|
|
507
|
+
return createErrorPayload(`job ${job_id}가 아직 실행 중입니다.`, { jobId: job_id, job_id });
|
|
508
|
+
}
|
|
509
|
+
if (job.providerRequested !== 'gemini' || job.transport !== 'gemini-worker') {
|
|
510
|
+
return createErrorPayload('delegate-reply는 현재 direct Gemini job에만 지원됩니다.', {
|
|
511
|
+
jobId: job_id,
|
|
512
|
+
job_id,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const conversation = this.geminiConversations.get(job_id);
|
|
517
|
+
if (!conversation) {
|
|
518
|
+
return createErrorPayload(`Gemini 대화 컨텍스트가 없습니다: ${job_id}`, { jobId: job_id, job_id });
|
|
519
|
+
}
|
|
520
|
+
if (conversation.closed) {
|
|
521
|
+
return createErrorPayload(`이미 종료된 대화입니다: ${job_id}`, { jobId: job_id, job_id });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
await emitProgress(extra, DIRECT_PROGRESS_START, 100, `job ${job_id} 후속 응답을 시작합니다.`);
|
|
525
|
+
job.status = 'running';
|
|
526
|
+
job.updatedAt = new Date().toISOString();
|
|
527
|
+
|
|
528
|
+
const worker = this._createGeminiWorker();
|
|
529
|
+
job.worker = worker;
|
|
530
|
+
const prompt = this._buildGeminiReplyPrompt(conversation, reply);
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const result = await worker.execute(prompt, {
|
|
534
|
+
cwd: job.requestArgs.cwd || this.cwd,
|
|
535
|
+
timeoutMs: resolveTimeoutMs(job.agentType, job.requestArgs.timeoutMs),
|
|
536
|
+
model: job.requestArgs.model || resolveGeminiModel(job.agentType),
|
|
537
|
+
approvalMode: 'yolo',
|
|
538
|
+
allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(job.requestArgs)),
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
conversation.turns.push({
|
|
542
|
+
user: reply,
|
|
543
|
+
assistant: result.output,
|
|
544
|
+
});
|
|
545
|
+
conversation.updatedAt = new Date().toISOString();
|
|
546
|
+
conversation.closed = Boolean(done);
|
|
547
|
+
if (done) {
|
|
548
|
+
this.geminiConversations.delete(job_id);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this._applyJobResult(job, {
|
|
552
|
+
ok: result.exitCode === 0,
|
|
553
|
+
status: result.exitCode === 0 ? 'completed' : 'failed',
|
|
554
|
+
providerRequested: 'gemini',
|
|
555
|
+
providerResolved: 'gemini',
|
|
556
|
+
agentType: job.agentType,
|
|
557
|
+
transport: 'gemini-worker',
|
|
558
|
+
exitCode: result.exitCode,
|
|
559
|
+
output: result.output,
|
|
560
|
+
sessionKey: result.sessionKey || job.sessionKey || null,
|
|
561
|
+
});
|
|
562
|
+
await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${job_id} 후속 응답이 완료되었습니다.`);
|
|
563
|
+
return this._serializeJob(job);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
566
|
+
this._applyJobResult(job, createErrorPayload(message, {
|
|
567
|
+
mode: job.mode,
|
|
568
|
+
providerRequested: 'gemini',
|
|
569
|
+
providerResolved: 'gemini',
|
|
570
|
+
agentType: job.agentType,
|
|
571
|
+
transport: 'gemini-worker',
|
|
572
|
+
}));
|
|
573
|
+
return this._serializeJob(job);
|
|
574
|
+
} finally {
|
|
575
|
+
await worker.stop().catch(() => {});
|
|
576
|
+
job.worker = null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
_createGeminiWorker() {
|
|
581
|
+
return new GeminiWorker({
|
|
582
|
+
command: this.geminiCommand,
|
|
583
|
+
commandArgs: this.geminiCommandArgs,
|
|
584
|
+
cwd: this.cwd,
|
|
585
|
+
env: this.env,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
_buildDirectPrompt(args) {
|
|
590
|
+
return withContext(String(args.prompt ?? ''), args.contextFile);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
_buildDirectPromptWithHint(args) {
|
|
594
|
+
return withPromptHint(String(args.prompt ?? ''), {
|
|
595
|
+
agentType: args.agentType || 'executor',
|
|
596
|
+
mcpProfile: args.mcpProfile || 'auto',
|
|
597
|
+
searchTool: args.searchTool,
|
|
598
|
+
workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
|
|
599
|
+
contextFile: args.contextFile,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
_buildGeminiReplyPrompt(conversation, reply) {
|
|
604
|
+
const transcript = formatConversationTranscript(conversation.turns);
|
|
605
|
+
return [
|
|
606
|
+
'Continue the conversation using the prior transcript below.',
|
|
607
|
+
'',
|
|
608
|
+
'<conversation_history>',
|
|
609
|
+
transcript,
|
|
610
|
+
'</conversation_history>',
|
|
611
|
+
'',
|
|
612
|
+
'<latest_user_reply>',
|
|
613
|
+
reply,
|
|
614
|
+
'</latest_user_reply>',
|
|
615
|
+
].join('\n');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
_getMcpPolicyOptions(args) {
|
|
619
|
+
return {
|
|
620
|
+
agentType: args.agentType || 'executor',
|
|
621
|
+
requestedProfile: args.mcpProfile || 'auto',
|
|
622
|
+
searchTool: args.searchTool,
|
|
623
|
+
workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
|
|
624
|
+
taskText: withContext(String(args.prompt ?? ''), args.contextFile),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
_buildPromptHintInstruction(args) {
|
|
629
|
+
return buildPromptHint(this._getMcpPolicyOptions(args));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
_shouldUseRoute(args) {
|
|
633
|
+
return args.provider === 'auto' || isTeamRouteRequested(args);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async _executeDirect(args, extra = null) {
|
|
637
|
+
await emitProgress(extra, DIRECT_PROGRESS_START, 100, '위임 실행을 시작합니다.');
|
|
638
|
+
|
|
639
|
+
const runViaRoute = this._shouldUseRoute(args);
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const result = runViaRoute
|
|
643
|
+
? await this._executeRoute(args, extra)
|
|
644
|
+
: await this._executeWorker(args, extra);
|
|
645
|
+
|
|
646
|
+
await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, '위임이 완료되었습니다.');
|
|
647
|
+
return result;
|
|
648
|
+
} catch (error) {
|
|
649
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
650
|
+
return createErrorPayload(message, {
|
|
651
|
+
mode: 'sync',
|
|
652
|
+
providerRequested: args.provider,
|
|
653
|
+
agentType: args.agentType,
|
|
654
|
+
transport: runViaRoute ? 'route-script' : `${args.provider}-worker`,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async _executeWorker(args, extra) {
|
|
660
|
+
await emitProgress(extra, DIRECT_PROGRESS_RUNNING, 100, '직접 워커 경로로 실행 중입니다.');
|
|
661
|
+
|
|
662
|
+
if (args.provider === 'codex') {
|
|
663
|
+
const result = await this.codexWorker.execute(this._buildDirectPrompt(args), {
|
|
664
|
+
cwd: args.cwd || this.cwd,
|
|
665
|
+
timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
|
|
666
|
+
sessionKey: args.sessionKey,
|
|
667
|
+
threadId: args.threadId,
|
|
668
|
+
resetSession: args.resetSession,
|
|
669
|
+
profile: resolveCodexProfile(args.agentType),
|
|
670
|
+
sandbox: 'danger-full-access',
|
|
671
|
+
approvalPolicy: 'never',
|
|
672
|
+
developerInstructions: joinInstructions(
|
|
673
|
+
REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
|
|
674
|
+
this._buildPromptHintInstruction(args),
|
|
675
|
+
args.developerInstructions,
|
|
676
|
+
),
|
|
677
|
+
config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
|
|
678
|
+
compactPrompt: args.compactPrompt,
|
|
679
|
+
model: args.model,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
ok: result.exitCode === 0,
|
|
684
|
+
mode: 'sync',
|
|
685
|
+
status: result.exitCode === 0 ? 'completed' : 'failed',
|
|
686
|
+
providerRequested: 'codex',
|
|
687
|
+
providerResolved: 'codex',
|
|
688
|
+
agentType: args.agentType,
|
|
689
|
+
transport: 'codex-mcp',
|
|
690
|
+
exitCode: result.exitCode,
|
|
691
|
+
output: result.output,
|
|
692
|
+
sessionKey: result.sessionKey,
|
|
693
|
+
threadId: result.threadId,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (args.provider === 'gemini') {
|
|
698
|
+
const worker = this._createGeminiWorker();
|
|
699
|
+
const prompt = this._buildDirectPromptWithHint(args);
|
|
700
|
+
try {
|
|
701
|
+
const result = await worker.execute(prompt, {
|
|
702
|
+
cwd: args.cwd || this.cwd,
|
|
703
|
+
timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
|
|
704
|
+
model: args.model || resolveGeminiModel(args.agentType),
|
|
705
|
+
approvalMode: 'yolo',
|
|
706
|
+
allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(args)),
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
ok: result.exitCode === 0,
|
|
711
|
+
mode: 'sync',
|
|
712
|
+
status: result.exitCode === 0 ? 'completed' : 'failed',
|
|
713
|
+
providerRequested: 'gemini',
|
|
714
|
+
providerResolved: 'gemini',
|
|
715
|
+
agentType: args.agentType,
|
|
716
|
+
transport: 'gemini-worker',
|
|
717
|
+
exitCode: result.exitCode,
|
|
718
|
+
output: result.output,
|
|
719
|
+
sessionKey: result.sessionKey,
|
|
720
|
+
_geminiPrompt: prompt,
|
|
721
|
+
};
|
|
722
|
+
} finally {
|
|
723
|
+
await worker.stop().catch(() => {});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return createErrorPayload(`지원하지 않는 direct provider: ${args.provider}`, {
|
|
728
|
+
mode: 'sync',
|
|
729
|
+
providerRequested: args.provider,
|
|
730
|
+
agentType: args.agentType,
|
|
731
|
+
transport: 'direct-worker',
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async _executeRoute(args, extra) {
|
|
736
|
+
if (!this.routeScript) {
|
|
737
|
+
return createErrorPayload('tfx-route.sh 경로를 찾지 못했습니다.', {
|
|
738
|
+
mode: 'sync',
|
|
739
|
+
providerRequested: args.provider,
|
|
740
|
+
agentType: args.agentType,
|
|
741
|
+
transport: 'route-script',
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
await emitProgress(extra, DIRECT_PROGRESS_RUNNING, 100, 'tfx-route.sh 경로로 실행 중입니다.');
|
|
746
|
+
const result = await this._spawnRoute(args);
|
|
747
|
+
return {
|
|
748
|
+
ok: result.exitCode === 0,
|
|
749
|
+
mode: 'sync',
|
|
750
|
+
status: result.exitCode === 0 ? 'completed' : 'failed',
|
|
751
|
+
providerRequested: args.provider,
|
|
752
|
+
providerResolved: parseRouteType(result.stderr) || args.provider,
|
|
753
|
+
agentType: args.agentType,
|
|
754
|
+
transport: 'route-script',
|
|
755
|
+
exitCode: result.exitCode,
|
|
756
|
+
output: result.stdout.trim() || result.stderr.trim(),
|
|
757
|
+
stderr: result.stderr.trim(),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async _startAsyncJob(args, extra) {
|
|
762
|
+
const job = this._createJob(args, 'async');
|
|
763
|
+
this.jobs.set(job.jobId, job);
|
|
764
|
+
|
|
765
|
+
await emitProgress(extra, DIRECT_PROGRESS_START, 100, `비동기 job ${job.jobId}를 시작합니다.`);
|
|
766
|
+
|
|
767
|
+
void (async () => {
|
|
768
|
+
try {
|
|
769
|
+
const result = this._shouldUseRoute(args)
|
|
770
|
+
? await this._spawnRoute(args, job)
|
|
771
|
+
: await this._runAsyncWorker(args, job);
|
|
772
|
+
this._applyJobResult(job, result);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
this._applyJobResult(job, createErrorPayload(
|
|
775
|
+
error instanceof Error ? error.message : String(error),
|
|
776
|
+
{
|
|
777
|
+
mode: 'async',
|
|
778
|
+
providerRequested: args.provider,
|
|
779
|
+
agentType: args.agentType,
|
|
780
|
+
transport: this._shouldUseRoute(args) ? 'route-script' : `${args.provider}-worker`,
|
|
781
|
+
},
|
|
782
|
+
));
|
|
783
|
+
} finally {
|
|
784
|
+
if (job.worker) {
|
|
785
|
+
await job.worker.stop().catch(() => {});
|
|
786
|
+
job.worker = null;
|
|
787
|
+
}
|
|
788
|
+
job.child = null;
|
|
789
|
+
}
|
|
790
|
+
})();
|
|
791
|
+
|
|
792
|
+
return this._serializeJob(job);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async _runAsyncWorker(args, job) {
|
|
796
|
+
if (args.provider === 'codex') {
|
|
797
|
+
const result = await this.codexWorker.execute(this._buildDirectPrompt(args), {
|
|
798
|
+
cwd: args.cwd || this.cwd,
|
|
799
|
+
timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
|
|
800
|
+
sessionKey: args.sessionKey,
|
|
801
|
+
threadId: args.threadId,
|
|
802
|
+
resetSession: args.resetSession,
|
|
803
|
+
profile: resolveCodexProfile(args.agentType),
|
|
804
|
+
sandbox: 'danger-full-access',
|
|
805
|
+
approvalPolicy: 'never',
|
|
806
|
+
developerInstructions: joinInstructions(
|
|
807
|
+
REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
|
|
808
|
+
this._buildPromptHintInstruction(args),
|
|
809
|
+
args.developerInstructions,
|
|
810
|
+
),
|
|
811
|
+
config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
|
|
812
|
+
compactPrompt: args.compactPrompt,
|
|
813
|
+
model: args.model,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
ok: result.exitCode === 0,
|
|
818
|
+
providerResolved: 'codex',
|
|
819
|
+
output: result.output,
|
|
820
|
+
exitCode: result.exitCode,
|
|
821
|
+
threadId: result.threadId,
|
|
822
|
+
sessionKey: result.sessionKey,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (args.provider === 'gemini') {
|
|
827
|
+
const worker = this._createGeminiWorker();
|
|
828
|
+
job.worker = worker;
|
|
829
|
+
const prompt = this._buildDirectPromptWithHint(args);
|
|
830
|
+
const result = await worker.execute(prompt, {
|
|
831
|
+
cwd: args.cwd || this.cwd,
|
|
832
|
+
timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
|
|
833
|
+
model: args.model || resolveGeminiModel(args.agentType),
|
|
834
|
+
approvalMode: 'yolo',
|
|
835
|
+
allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(args)),
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
return {
|
|
839
|
+
ok: result.exitCode === 0,
|
|
840
|
+
providerResolved: 'gemini',
|
|
841
|
+
output: result.output,
|
|
842
|
+
exitCode: result.exitCode,
|
|
843
|
+
sessionKey: result.sessionKey,
|
|
844
|
+
_geminiPrompt: prompt,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
throw new Error(`지원하지 않는 async provider: ${args.provider}`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
_buildRouteEnv(args) {
|
|
852
|
+
const env = cloneEnv(this.env);
|
|
853
|
+
env.TFX_CLI_MODE = pickRouteMode(args.provider);
|
|
854
|
+
|
|
855
|
+
if (args.codexTransport) {
|
|
856
|
+
env.TFX_CODEX_TRANSPORT = args.codexTransport;
|
|
857
|
+
}
|
|
858
|
+
if (args.noClaudeNative === true) {
|
|
859
|
+
env.TFX_NO_CLAUDE_NATIVE = '1';
|
|
860
|
+
}
|
|
861
|
+
if (args.searchTool) {
|
|
862
|
+
env.TFX_SEARCH_TOOL = args.searchTool;
|
|
863
|
+
}
|
|
864
|
+
if (Number.isInteger(args.workerIndex) && args.workerIndex > 0) {
|
|
865
|
+
env.TFX_WORKER_INDEX = String(args.workerIndex);
|
|
866
|
+
}
|
|
867
|
+
if (args.teamName) env.TFX_TEAM_NAME = args.teamName;
|
|
868
|
+
if (args.teamTaskId) env.TFX_TEAM_TASK_ID = args.teamTaskId;
|
|
869
|
+
if (args.teamAgentName) env.TFX_TEAM_AGENT_NAME = args.teamAgentName;
|
|
870
|
+
if (args.teamLeadName) env.TFX_TEAM_LEAD_NAME = args.teamLeadName;
|
|
871
|
+
if (args.hubUrl) env.TFX_HUB_URL = args.hubUrl;
|
|
872
|
+
|
|
873
|
+
return env;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async _spawnRoute(args, job = null) {
|
|
877
|
+
const prompt = withContext(String(args.prompt ?? ''), args.contextFile);
|
|
878
|
+
const childArgs = [
|
|
879
|
+
this.routeScript,
|
|
880
|
+
args.agentType || 'executor',
|
|
881
|
+
prompt,
|
|
882
|
+
args.mcpProfile || 'auto',
|
|
883
|
+
String(resolveTimeoutSec(args.agentType, args.timeoutMs)),
|
|
884
|
+
];
|
|
885
|
+
|
|
886
|
+
const child = spawn(this.bashCommand, childArgs, {
|
|
887
|
+
cwd: args.cwd || this.cwd,
|
|
888
|
+
env: this._buildRouteEnv(args),
|
|
889
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
890
|
+
windowsHide: true,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
if (job) {
|
|
894
|
+
job.child = child;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
this.routeChildren.add(child);
|
|
898
|
+
|
|
899
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
900
|
+
const stdoutChunks = [];
|
|
901
|
+
const stderrChunks = [];
|
|
902
|
+
|
|
903
|
+
child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
|
904
|
+
child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
905
|
+
child.once('error', (error) => {
|
|
906
|
+
this.routeChildren.delete(child);
|
|
907
|
+
rejectPromise(error);
|
|
908
|
+
});
|
|
909
|
+
child.once('close', (code) => {
|
|
910
|
+
this.routeChildren.delete(child);
|
|
911
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
912
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
913
|
+
resolvePromise({
|
|
914
|
+
ok: code === 0,
|
|
915
|
+
providerResolved: parseRouteType(stderr) || args.provider,
|
|
916
|
+
output: stdout.trim() || stderr.trim(),
|
|
917
|
+
stdout,
|
|
918
|
+
stderr,
|
|
919
|
+
exitCode: code ?? 1,
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
_serializeJob(job) {
|
|
926
|
+
return {
|
|
927
|
+
ok: job.ok,
|
|
928
|
+
jobId: job.jobId,
|
|
929
|
+
job_id: job.jobId,
|
|
930
|
+
mode: job.mode || 'async',
|
|
931
|
+
status: job.status,
|
|
932
|
+
providerRequested: job.providerRequested,
|
|
933
|
+
providerResolved: job.providerResolved,
|
|
934
|
+
agentType: job.agentType,
|
|
935
|
+
transport: job.transport,
|
|
936
|
+
createdAt: job.createdAt,
|
|
937
|
+
startedAt: job.startedAt,
|
|
938
|
+
updatedAt: job.updatedAt,
|
|
939
|
+
completedAt: job.completedAt,
|
|
940
|
+
exitCode: job.exitCode,
|
|
941
|
+
output: job.output,
|
|
942
|
+
stderr: job.stderr,
|
|
943
|
+
threadId: job.threadId,
|
|
944
|
+
sessionKey: job.sessionKey,
|
|
945
|
+
conversationOpen: this.geminiConversations.has(job.jobId),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
_createJob(args, mode) {
|
|
950
|
+
const jobId = randomUUID();
|
|
951
|
+
const now = new Date().toISOString();
|
|
952
|
+
return {
|
|
953
|
+
ok: true,
|
|
954
|
+
jobId,
|
|
955
|
+
mode,
|
|
956
|
+
status: 'running',
|
|
957
|
+
providerRequested: args.provider,
|
|
958
|
+
providerResolved: null,
|
|
959
|
+
agentType: args.agentType,
|
|
960
|
+
transport: this._shouldUseRoute(args) ? 'route-script' : `${args.provider}-worker`,
|
|
961
|
+
createdAt: now,
|
|
962
|
+
startedAt: now,
|
|
963
|
+
updatedAt: now,
|
|
964
|
+
completedAt: null,
|
|
965
|
+
output: '',
|
|
966
|
+
stderr: '',
|
|
967
|
+
exitCode: null,
|
|
968
|
+
threadId: null,
|
|
969
|
+
sessionKey: args.sessionKey || null,
|
|
970
|
+
worker: null,
|
|
971
|
+
child: null,
|
|
972
|
+
requestArgs: sanitizeDelegateArgs(args),
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
_applyJobResult(job, result = {}) {
|
|
977
|
+
job.ok = result.ok !== false;
|
|
978
|
+
job.status = job.ok ? 'completed' : 'failed';
|
|
979
|
+
job.providerResolved = result.providerResolved || job.providerRequested;
|
|
980
|
+
job.transport = result.transport || job.transport;
|
|
981
|
+
job.output = result.output || '';
|
|
982
|
+
job.stderr = result.stderr || result.error || '';
|
|
983
|
+
job.exitCode = result.exitCode ?? (job.ok ? 0 : 1);
|
|
984
|
+
job.threadId = result.threadId || job.threadId || null;
|
|
985
|
+
job.sessionKey = result.sessionKey || job.sessionKey || null;
|
|
986
|
+
job.completedAt = new Date().toISOString();
|
|
987
|
+
job.updatedAt = job.completedAt;
|
|
988
|
+
|
|
989
|
+
if (job.providerRequested === 'gemini'
|
|
990
|
+
&& job.transport === 'gemini-worker'
|
|
991
|
+
&& typeof result._geminiPrompt === 'string') {
|
|
992
|
+
this._storeGeminiConversation(job, result._geminiPrompt, result.output || '');
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
_storeGeminiConversation(job, userPrompt, assistantReply) {
|
|
997
|
+
const existing = this.geminiConversations.get(job.jobId);
|
|
998
|
+
if (existing) {
|
|
999
|
+
if (typeof assistantReply === 'string') {
|
|
1000
|
+
const lastTurn = existing.turns.at(-1);
|
|
1001
|
+
if (lastTurn && lastTurn.assistant !== assistantReply) {
|
|
1002
|
+
lastTurn.assistant = assistantReply;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
existing.updatedAt = new Date().toISOString();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
this.geminiConversations.set(job.jobId, {
|
|
1010
|
+
jobId: job.jobId,
|
|
1011
|
+
closed: false,
|
|
1012
|
+
updatedAt: new Date().toISOString(),
|
|
1013
|
+
turns: [{
|
|
1014
|
+
user: userPrompt,
|
|
1015
|
+
assistant: assistantReply,
|
|
1016
|
+
}],
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async _runSyncJob(args, extra) {
|
|
1021
|
+
const job = this._createJob(args, 'sync');
|
|
1022
|
+
this.jobs.set(job.jobId, job);
|
|
1023
|
+
const result = await this._executeDirect(args, extra);
|
|
1024
|
+
this._applyJobResult(job, result);
|
|
1025
|
+
return this._serializeJob(job);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function createDelegatorMcpWorker(options = {}) {
|
|
1030
|
+
return new DelegatorMcpWorker(options);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export async function runDelegatorMcpCli() {
|
|
1034
|
+
const worker = createDelegatorMcpWorker();
|
|
1035
|
+
try {
|
|
1036
|
+
await worker.serveStdio();
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
console.error(`[delegator-mcp] ${error instanceof Error ? error.message : String(error)}`);
|
|
1039
|
+
process.exitCode = 1;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
1044
|
+
await runDelegatorMcpCli();
|
|
1045
|
+
}
|