triflux 3.3.0-dev.6 → 3.3.0-dev.8
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 +20 -2
- package/hub/bridge.mjs +68 -92
- package/hub/delegator/contracts.mjs +38 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +118 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/team/native.mjs +101 -0
- package/package.json +1 -1
package/bin/triflux.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// triflux CLI — setup, doctor, version
|
|
3
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync } from "fs";
|
|
3
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync, openSync, closeSync } from "fs";
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { execSync, execFileSync, spawn } from "child_process";
|
|
@@ -1408,7 +1408,25 @@ function stopHubForUpdate() {
|
|
|
1408
1408
|
try { process.kill(info.pid, "SIGKILL"); } catch {}
|
|
1409
1409
|
}
|
|
1410
1410
|
|
|
1411
|
-
|
|
1411
|
+
// Windows에서 better-sqlite3.node 파일 핸들 해제 대기
|
|
1412
|
+
// taskkill 후 프로세스 종료 + 파일 핸들 해제까지 최대 5초
|
|
1413
|
+
const sqliteNode = join(PKG_ROOT, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node");
|
|
1414
|
+
for (let i = 0; i < 10; i++) {
|
|
1415
|
+
sleepMs(500);
|
|
1416
|
+
try { process.kill(info.pid, 0); } catch { break; }
|
|
1417
|
+
}
|
|
1418
|
+
// 파일 잠금 해제 확인 (Windows EBUSY 방지)
|
|
1419
|
+
if (existsSync(sqliteNode)) {
|
|
1420
|
+
for (let i = 0; i < 6; i++) {
|
|
1421
|
+
try {
|
|
1422
|
+
const fd = openSync(sqliteNode, "r");
|
|
1423
|
+
closeSync(fd);
|
|
1424
|
+
break;
|
|
1425
|
+
} catch {
|
|
1426
|
+
sleepMs(500);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1412
1430
|
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1413
1431
|
return info;
|
|
1414
1432
|
}
|
package/hub/bridge.mjs
CHANGED
|
@@ -295,6 +295,17 @@ async function requestHub(operation, body, timeoutMs = 3000, fallback = null) {
|
|
|
295
295
|
return { transport: 'fallback', result: viaFallback };
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
function unavailableResult() {
|
|
299
|
+
return { ok: false, reason: 'hub_unavailable' };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function emitJson(payload) {
|
|
303
|
+
if (payload !== undefined) {
|
|
304
|
+
console.log(JSON.stringify(payload));
|
|
305
|
+
}
|
|
306
|
+
return payload?.ok !== false;
|
|
307
|
+
}
|
|
308
|
+
|
|
298
309
|
async function cmdRegister(args) {
|
|
299
310
|
const agentId = args.agent;
|
|
300
311
|
const timeoutSec = parseInt(args.timeout || '600', 10);
|
|
@@ -313,15 +324,15 @@ async function cmdRegister(args) {
|
|
|
313
324
|
const result = outcome?.result;
|
|
314
325
|
|
|
315
326
|
if (result?.ok) {
|
|
316
|
-
|
|
327
|
+
return emitJson({
|
|
317
328
|
ok: true,
|
|
318
329
|
agent_id: agentId,
|
|
319
330
|
lease_expires_ms: result.data?.lease_expires_ms,
|
|
320
331
|
pipe_path: result.data?.pipe_path || getHubPipePath(),
|
|
321
|
-
})
|
|
322
|
-
} else {
|
|
323
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
332
|
+
});
|
|
324
333
|
}
|
|
334
|
+
|
|
335
|
+
return emitJson(result || unavailableResult());
|
|
325
336
|
}
|
|
326
337
|
|
|
327
338
|
async function cmdResult(args) {
|
|
@@ -349,10 +360,10 @@ async function cmdResult(args) {
|
|
|
349
360
|
const result = outcome?.result;
|
|
350
361
|
|
|
351
362
|
if (result?.ok) {
|
|
352
|
-
|
|
353
|
-
} else {
|
|
354
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
363
|
+
return emitJson({ ok: true, message_id: result.data?.message_id });
|
|
355
364
|
}
|
|
365
|
+
|
|
366
|
+
return emitJson(result || unavailableResult());
|
|
356
367
|
}
|
|
357
368
|
|
|
358
369
|
async function cmdControl(args) {
|
|
@@ -367,11 +378,7 @@ async function cmdControl(args) {
|
|
|
367
378
|
ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
|
|
368
379
|
});
|
|
369
380
|
const result = outcome?.result;
|
|
370
|
-
|
|
371
|
-
console.log(JSON.stringify(result));
|
|
372
|
-
} else {
|
|
373
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
374
|
-
}
|
|
381
|
+
return emitJson(result || unavailableResult());
|
|
375
382
|
}
|
|
376
383
|
|
|
377
384
|
async function cmdContext(args) {
|
|
@@ -394,14 +401,21 @@ async function cmdContext(args) {
|
|
|
394
401
|
|
|
395
402
|
if (args.out) {
|
|
396
403
|
writeFileSync(args.out, combined, 'utf8');
|
|
397
|
-
|
|
404
|
+
return emitJson({ ok: true, count: result.data.messages.length, file: args.out });
|
|
398
405
|
} else {
|
|
399
406
|
console.log(combined);
|
|
407
|
+
return true;
|
|
400
408
|
}
|
|
401
|
-
return;
|
|
402
409
|
}
|
|
403
410
|
|
|
404
|
-
if (
|
|
411
|
+
if (result?.ok) {
|
|
412
|
+
if (args.out) {
|
|
413
|
+
return emitJson({ ok: true, count: 0 });
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return emitJson(result || unavailableResult());
|
|
405
419
|
}
|
|
406
420
|
|
|
407
421
|
async function cmdDeregister(args) {
|
|
@@ -411,10 +425,10 @@ async function cmdDeregister(args) {
|
|
|
411
425
|
const result = outcome?.result;
|
|
412
426
|
|
|
413
427
|
if (result?.ok) {
|
|
414
|
-
|
|
415
|
-
} else {
|
|
416
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
428
|
+
return emitJson({ ok: true, agent_id: args.agent, status: 'offline' });
|
|
417
429
|
}
|
|
430
|
+
|
|
431
|
+
return emitJson(result || unavailableResult());
|
|
418
432
|
}
|
|
419
433
|
|
|
420
434
|
async function cmdAssignAsync(args) {
|
|
@@ -432,11 +446,7 @@ async function cmdAssignAsync(args) {
|
|
|
432
446
|
correlation_id: args.correlation || undefined,
|
|
433
447
|
});
|
|
434
448
|
const result = outcome?.result;
|
|
435
|
-
|
|
436
|
-
console.log(JSON.stringify(result));
|
|
437
|
-
} else {
|
|
438
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
439
|
-
}
|
|
449
|
+
return emitJson(result || unavailableResult());
|
|
440
450
|
}
|
|
441
451
|
|
|
442
452
|
async function cmdAssignResult(args) {
|
|
@@ -451,11 +461,7 @@ async function cmdAssignResult(args) {
|
|
|
451
461
|
metadata: args.metadata ? parseJsonSafe(args.metadata, {}) : {},
|
|
452
462
|
});
|
|
453
463
|
const result = outcome?.result;
|
|
454
|
-
|
|
455
|
-
console.log(JSON.stringify(result));
|
|
456
|
-
} else {
|
|
457
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
458
|
-
}
|
|
464
|
+
return emitJson(result || unavailableResult());
|
|
459
465
|
}
|
|
460
466
|
|
|
461
467
|
async function cmdAssignStatus(args) {
|
|
@@ -463,11 +469,7 @@ async function cmdAssignStatus(args) {
|
|
|
463
469
|
job_id: args['job-id'],
|
|
464
470
|
});
|
|
465
471
|
const result = outcome?.result;
|
|
466
|
-
|
|
467
|
-
console.log(JSON.stringify(result));
|
|
468
|
-
} else {
|
|
469
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
470
|
-
}
|
|
472
|
+
return emitJson(result || unavailableResult());
|
|
471
473
|
}
|
|
472
474
|
|
|
473
475
|
async function cmdAssignRetry(args) {
|
|
@@ -477,11 +479,7 @@ async function cmdAssignRetry(args) {
|
|
|
477
479
|
requested_by: args['requested-by'],
|
|
478
480
|
});
|
|
479
481
|
const result = outcome?.result;
|
|
480
|
-
|
|
481
|
-
console.log(JSON.stringify(result));
|
|
482
|
-
} else {
|
|
483
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
484
|
-
}
|
|
482
|
+
return emitJson(result || unavailableResult());
|
|
485
483
|
}
|
|
486
484
|
|
|
487
485
|
async function cmdTeamInfo(args) {
|
|
@@ -495,9 +493,7 @@ async function cmdTeamInfo(args) {
|
|
|
495
493
|
return await teamInfo(body);
|
|
496
494
|
});
|
|
497
495
|
const result = outcome?.result;
|
|
498
|
-
|
|
499
|
-
console.log(JSON.stringify(result));
|
|
500
|
-
}
|
|
496
|
+
return emitJson(result || unavailableResult());
|
|
501
497
|
}
|
|
502
498
|
|
|
503
499
|
async function cmdTeamTaskList(args) {
|
|
@@ -513,9 +509,7 @@ async function cmdTeamTaskList(args) {
|
|
|
513
509
|
return await teamTaskList(body);
|
|
514
510
|
});
|
|
515
511
|
const result = outcome?.result;
|
|
516
|
-
|
|
517
|
-
console.log(JSON.stringify(result));
|
|
518
|
-
}
|
|
512
|
+
return emitJson(result || unavailableResult());
|
|
519
513
|
}
|
|
520
514
|
|
|
521
515
|
async function cmdTeamTaskUpdate(args) {
|
|
@@ -539,9 +533,7 @@ async function cmdTeamTaskUpdate(args) {
|
|
|
539
533
|
return await teamTaskUpdate(body);
|
|
540
534
|
});
|
|
541
535
|
const result = outcome?.result;
|
|
542
|
-
|
|
543
|
-
console.log(JSON.stringify(result));
|
|
544
|
-
}
|
|
536
|
+
return emitJson(result || unavailableResult());
|
|
545
537
|
}
|
|
546
538
|
|
|
547
539
|
async function cmdTeamSendMessage(args) {
|
|
@@ -558,9 +550,7 @@ async function cmdTeamSendMessage(args) {
|
|
|
558
550
|
return await teamSendMessage(body);
|
|
559
551
|
});
|
|
560
552
|
const result = outcome?.result;
|
|
561
|
-
|
|
562
|
-
console.log(JSON.stringify(result));
|
|
563
|
-
}
|
|
553
|
+
return emitJson(result || unavailableResult());
|
|
564
554
|
}
|
|
565
555
|
|
|
566
556
|
function getHubDbPath() {
|
|
@@ -588,9 +578,7 @@ async function cmdPipelineState(args) {
|
|
|
588
578
|
}
|
|
589
579
|
});
|
|
590
580
|
const result = outcome?.result;
|
|
591
|
-
|
|
592
|
-
console.log(JSON.stringify(result));
|
|
593
|
-
}
|
|
581
|
+
return emitJson(result || unavailableResult());
|
|
594
582
|
}
|
|
595
583
|
|
|
596
584
|
async function cmdPipelineAdvance(args) {
|
|
@@ -616,9 +604,7 @@ async function cmdPipelineAdvance(args) {
|
|
|
616
604
|
}
|
|
617
605
|
});
|
|
618
606
|
const result = outcome?.result;
|
|
619
|
-
|
|
620
|
-
console.log(JSON.stringify(result));
|
|
621
|
-
}
|
|
607
|
+
return emitJson(result || unavailableResult());
|
|
622
608
|
}
|
|
623
609
|
|
|
624
610
|
async function cmdPipelineInit(args) {
|
|
@@ -628,49 +614,39 @@ async function cmdPipelineInit(args) {
|
|
|
628
614
|
ralph_max: args['ralph-max'] != null ? Number(args['ralph-max']) : undefined,
|
|
629
615
|
});
|
|
630
616
|
const result = outcome?.result;
|
|
631
|
-
|
|
632
|
-
console.log(JSON.stringify(result));
|
|
633
|
-
} else {
|
|
634
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
635
|
-
}
|
|
617
|
+
return emitJson(result || unavailableResult());
|
|
636
618
|
}
|
|
637
619
|
|
|
638
620
|
async function cmdPipelineList() {
|
|
639
621
|
const outcome = await requestHub(HUB_OPERATIONS.pipelineList, {});
|
|
640
622
|
const result = outcome?.result;
|
|
641
|
-
|
|
642
|
-
console.log(JSON.stringify(result));
|
|
643
|
-
} else {
|
|
644
|
-
console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
|
|
645
|
-
}
|
|
623
|
+
return emitJson(result || unavailableResult());
|
|
646
624
|
}
|
|
647
625
|
|
|
648
626
|
async function cmdPing() {
|
|
649
627
|
const outcome = await requestHub(HUB_OPERATIONS.hubStatus, { scope: 'hub' }, 2000);
|
|
650
628
|
|
|
651
629
|
if (outcome?.transport === 'pipe' && outcome.result?.ok) {
|
|
652
|
-
|
|
630
|
+
return emitJson({
|
|
653
631
|
ok: true,
|
|
654
632
|
hub: outcome.result.data?.hub?.state || 'healthy',
|
|
655
633
|
pipe_path: getHubPipePath(),
|
|
656
634
|
transport: 'pipe',
|
|
657
|
-
})
|
|
658
|
-
return;
|
|
635
|
+
});
|
|
659
636
|
}
|
|
660
637
|
|
|
661
638
|
if (outcome?.transport === 'http' && outcome.result) {
|
|
662
639
|
const data = outcome.result;
|
|
663
|
-
|
|
640
|
+
return emitJson({
|
|
664
641
|
ok: true,
|
|
665
642
|
hub: data.hub?.state,
|
|
666
643
|
sessions: data.sessions,
|
|
667
644
|
pipe_path: data.pipe?.path || data.pipe_path || null,
|
|
668
645
|
transport: 'http',
|
|
669
|
-
})
|
|
670
|
-
return;
|
|
646
|
+
});
|
|
671
647
|
}
|
|
672
648
|
|
|
673
|
-
|
|
649
|
+
return emitJson(unavailableResult());
|
|
674
650
|
}
|
|
675
651
|
|
|
676
652
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -678,24 +654,24 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
678
654
|
const args = parseArgs(argv.slice(1));
|
|
679
655
|
|
|
680
656
|
switch (cmd) {
|
|
681
|
-
case 'register': await cmdRegister(args);
|
|
682
|
-
case 'result': await cmdResult(args);
|
|
683
|
-
case 'control': await cmdControl(args);
|
|
684
|
-
case 'context': await cmdContext(args);
|
|
685
|
-
case 'deregister': await cmdDeregister(args);
|
|
686
|
-
case 'assign-async': await cmdAssignAsync(args);
|
|
687
|
-
case 'assign-result': await cmdAssignResult(args);
|
|
688
|
-
case 'assign-status': await cmdAssignStatus(args);
|
|
689
|
-
case 'assign-retry': await cmdAssignRetry(args);
|
|
690
|
-
case 'team-info': await cmdTeamInfo(args);
|
|
691
|
-
case 'team-task-list': await cmdTeamTaskList(args);
|
|
692
|
-
case 'team-task-update': await cmdTeamTaskUpdate(args);
|
|
693
|
-
case 'team-send-message': await cmdTeamSendMessage(args);
|
|
694
|
-
case 'pipeline-state': await cmdPipelineState(args);
|
|
695
|
-
case 'pipeline-advance': await cmdPipelineAdvance(args);
|
|
696
|
-
case 'pipeline-init': await cmdPipelineInit(args);
|
|
697
|
-
case 'pipeline-list': await cmdPipelineList(args);
|
|
698
|
-
case 'ping': await cmdPing(args);
|
|
657
|
+
case 'register': return await cmdRegister(args);
|
|
658
|
+
case 'result': return await cmdResult(args);
|
|
659
|
+
case 'control': return await cmdControl(args);
|
|
660
|
+
case 'context': return await cmdContext(args);
|
|
661
|
+
case 'deregister': return await cmdDeregister(args);
|
|
662
|
+
case 'assign-async': return await cmdAssignAsync(args);
|
|
663
|
+
case 'assign-result': return await cmdAssignResult(args);
|
|
664
|
+
case 'assign-status': return await cmdAssignStatus(args);
|
|
665
|
+
case 'assign-retry': return await cmdAssignRetry(args);
|
|
666
|
+
case 'team-info': return await cmdTeamInfo(args);
|
|
667
|
+
case 'team-task-list': return await cmdTeamTaskList(args);
|
|
668
|
+
case 'team-task-update': return await cmdTeamTaskUpdate(args);
|
|
669
|
+
case 'team-send-message': return await cmdTeamSendMessage(args);
|
|
670
|
+
case 'pipeline-state': return await cmdPipelineState(args);
|
|
671
|
+
case 'pipeline-advance': return await cmdPipelineAdvance(args);
|
|
672
|
+
case 'pipeline-init': return await cmdPipelineInit(args);
|
|
673
|
+
case 'pipeline-list': return await cmdPipelineList(args);
|
|
674
|
+
case 'ping': return await cmdPing(args);
|
|
699
675
|
default:
|
|
700
676
|
console.error('사용법: bridge.mjs <register|result|control|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping> [--옵션]');
|
|
701
677
|
process.exit(1);
|
|
@@ -704,5 +680,5 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
704
680
|
|
|
705
681
|
const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/bridge.mjs');
|
|
706
682
|
if (selfRun) {
|
|
707
|
-
await main();
|
|
683
|
+
process.exitCode = await main() ? 0 : 1;
|
|
708
684
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const DELEGATOR_MCP_SERVER_INFO = Object.freeze({
|
|
2
|
+
name: 'triflux-delegator',
|
|
3
|
+
version: '0.1.0',
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
export const DELEGATOR_TOOL_NAMES = Object.freeze({
|
|
7
|
+
delegate: 'delegate',
|
|
8
|
+
delegateReply: 'delegate-reply',
|
|
9
|
+
status: 'status',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const DELEGATOR_PIPE_ACTIONS = Object.freeze({
|
|
13
|
+
delegate: 'delegator_delegate',
|
|
14
|
+
delegateReply: 'delegator_reply',
|
|
15
|
+
status: 'delegator_status',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const DELEGATOR_JOB_STATUSES = Object.freeze([
|
|
19
|
+
'queued',
|
|
20
|
+
'running',
|
|
21
|
+
'waiting_reply',
|
|
22
|
+
'completed',
|
|
23
|
+
'failed',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export const DELEGATOR_MODES = Object.freeze([
|
|
27
|
+
'sync',
|
|
28
|
+
'async',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const DELEGATOR_PROVIDERS = Object.freeze([
|
|
32
|
+
'auto',
|
|
33
|
+
'codex',
|
|
34
|
+
'gemini',
|
|
35
|
+
'claude',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export const DELEGATOR_SCHEMA_URL = new URL('./schema/delegator-tools.schema.json', import.meta.url);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DELEGATOR_JOB_STATUSES,
|
|
3
|
+
DELEGATOR_MCP_SERVER_INFO,
|
|
4
|
+
DELEGATOR_MODES,
|
|
5
|
+
DELEGATOR_PIPE_ACTIONS,
|
|
6
|
+
DELEGATOR_PROVIDERS,
|
|
7
|
+
DELEGATOR_SCHEMA_URL,
|
|
8
|
+
DELEGATOR_TOOL_NAMES,
|
|
9
|
+
} from './contracts.mjs';
|
|
10
|
+
export { DelegatorService } from './service.mjs';
|
|
11
|
+
export {
|
|
12
|
+
getDelegatorMcpToolDefinitions,
|
|
13
|
+
loadDelegatorSchemaBundle,
|
|
14
|
+
} from './tool-definitions.mjs';
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://triflux.dev/schema/hub/delegator-tools.schema.json",
|
|
4
|
+
"title": "Triflux Delegator MCP Tool Schemas",
|
|
5
|
+
"$defs": {
|
|
6
|
+
"Provider": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"enum": ["auto", "codex", "gemini", "claude"]
|
|
9
|
+
},
|
|
10
|
+
"Mode": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": ["sync", "async"]
|
|
13
|
+
},
|
|
14
|
+
"JobStatus": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["queued", "running", "waiting_reply", "completed", "failed"]
|
|
17
|
+
},
|
|
18
|
+
"McpProfile": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"enum": [
|
|
21
|
+
"auto",
|
|
22
|
+
"default",
|
|
23
|
+
"executor",
|
|
24
|
+
"designer",
|
|
25
|
+
"explore",
|
|
26
|
+
"reviewer",
|
|
27
|
+
"writer",
|
|
28
|
+
"none",
|
|
29
|
+
"implement",
|
|
30
|
+
"analyze",
|
|
31
|
+
"review",
|
|
32
|
+
"docs",
|
|
33
|
+
"minimal"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"SearchTool": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["exa", "brave-search", "tavily"]
|
|
39
|
+
},
|
|
40
|
+
"NullableDateTime": {
|
|
41
|
+
"type": ["string", "null"],
|
|
42
|
+
"format": "date-time"
|
|
43
|
+
},
|
|
44
|
+
"NullableString": {
|
|
45
|
+
"type": ["string", "null"]
|
|
46
|
+
},
|
|
47
|
+
"DelegateInput": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"required": ["prompt"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"prompt": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"minLength": 1
|
|
55
|
+
},
|
|
56
|
+
"provider": {
|
|
57
|
+
"$ref": "#/$defs/Provider",
|
|
58
|
+
"default": "auto"
|
|
59
|
+
},
|
|
60
|
+
"mode": {
|
|
61
|
+
"$ref": "#/$defs/Mode",
|
|
62
|
+
"default": "sync"
|
|
63
|
+
},
|
|
64
|
+
"agent_type": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"default": "executor",
|
|
67
|
+
"minLength": 1
|
|
68
|
+
},
|
|
69
|
+
"cwd": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"minLength": 1
|
|
72
|
+
},
|
|
73
|
+
"timeout_ms": {
|
|
74
|
+
"type": "integer",
|
|
75
|
+
"minimum": 1
|
|
76
|
+
},
|
|
77
|
+
"session_key": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"minLength": 1
|
|
80
|
+
},
|
|
81
|
+
"thread_id": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"minLength": 1
|
|
84
|
+
},
|
|
85
|
+
"reset_session": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"default": false
|
|
88
|
+
},
|
|
89
|
+
"mcp_profile": {
|
|
90
|
+
"$ref": "#/$defs/McpProfile",
|
|
91
|
+
"default": "auto"
|
|
92
|
+
},
|
|
93
|
+
"search_tool": {
|
|
94
|
+
"$ref": "#/$defs/SearchTool"
|
|
95
|
+
},
|
|
96
|
+
"context_file": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"minLength": 1
|
|
99
|
+
},
|
|
100
|
+
"model": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"minLength": 1
|
|
103
|
+
},
|
|
104
|
+
"developer_instructions": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"minLength": 1
|
|
107
|
+
},
|
|
108
|
+
"compact_prompt": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"minLength": 1
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"DelegateReplyInput": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"additionalProperties": false,
|
|
117
|
+
"required": ["job_id", "reply"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"job_id": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"minLength": 1
|
|
122
|
+
},
|
|
123
|
+
"reply": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"minLength": 1
|
|
126
|
+
},
|
|
127
|
+
"done": {
|
|
128
|
+
"type": "boolean",
|
|
129
|
+
"default": false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"StatusInput": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": false,
|
|
136
|
+
"required": ["job_id"],
|
|
137
|
+
"properties": {
|
|
138
|
+
"job_id": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"minLength": 1
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"DelegateOutput": {
|
|
145
|
+
"type": "object",
|
|
146
|
+
"additionalProperties": false,
|
|
147
|
+
"required": [
|
|
148
|
+
"ok",
|
|
149
|
+
"job_id",
|
|
150
|
+
"status",
|
|
151
|
+
"mode",
|
|
152
|
+
"provider_requested",
|
|
153
|
+
"provider_resolved",
|
|
154
|
+
"agent_type",
|
|
155
|
+
"transport",
|
|
156
|
+
"created_at",
|
|
157
|
+
"started_at",
|
|
158
|
+
"updated_at",
|
|
159
|
+
"completed_at",
|
|
160
|
+
"thread_id",
|
|
161
|
+
"session_key",
|
|
162
|
+
"conversation_open"
|
|
163
|
+
],
|
|
164
|
+
"properties": {
|
|
165
|
+
"ok": {
|
|
166
|
+
"type": "boolean"
|
|
167
|
+
},
|
|
168
|
+
"job_id": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"minLength": 1
|
|
171
|
+
},
|
|
172
|
+
"status": {
|
|
173
|
+
"$ref": "#/$defs/JobStatus"
|
|
174
|
+
},
|
|
175
|
+
"mode": {
|
|
176
|
+
"$ref": "#/$defs/Mode"
|
|
177
|
+
},
|
|
178
|
+
"provider_requested": {
|
|
179
|
+
"$ref": "#/$defs/Provider"
|
|
180
|
+
},
|
|
181
|
+
"provider_resolved": {
|
|
182
|
+
"type": ["string", "null"]
|
|
183
|
+
},
|
|
184
|
+
"agent_type": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"minLength": 1
|
|
187
|
+
},
|
|
188
|
+
"transport": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"minLength": 1
|
|
191
|
+
},
|
|
192
|
+
"created_at": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"format": "date-time"
|
|
195
|
+
},
|
|
196
|
+
"started_at": {
|
|
197
|
+
"$ref": "#/$defs/NullableDateTime"
|
|
198
|
+
},
|
|
199
|
+
"updated_at": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"format": "date-time"
|
|
202
|
+
},
|
|
203
|
+
"completed_at": {
|
|
204
|
+
"$ref": "#/$defs/NullableDateTime"
|
|
205
|
+
},
|
|
206
|
+
"output": {
|
|
207
|
+
"type": "string"
|
|
208
|
+
},
|
|
209
|
+
"stderr": {
|
|
210
|
+
"type": "string"
|
|
211
|
+
},
|
|
212
|
+
"error": {
|
|
213
|
+
"type": "string"
|
|
214
|
+
},
|
|
215
|
+
"thread_id": {
|
|
216
|
+
"$ref": "#/$defs/NullableString"
|
|
217
|
+
},
|
|
218
|
+
"session_key": {
|
|
219
|
+
"$ref": "#/$defs/NullableString"
|
|
220
|
+
},
|
|
221
|
+
"conversation_open": {
|
|
222
|
+
"type": "boolean"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"x-triflux-mcp-tools": [
|
|
228
|
+
{
|
|
229
|
+
"name": "delegate",
|
|
230
|
+
"description": "Create a new delegator job and optionally wait for the first result.",
|
|
231
|
+
"inputSchemaDef": "DelegateInput",
|
|
232
|
+
"outputSchemaDef": "DelegateOutput",
|
|
233
|
+
"pipeAction": "delegator_delegate"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "delegate-reply",
|
|
237
|
+
"description": "Send a follow-up reply into an existing delegator conversation job.",
|
|
238
|
+
"inputSchemaDef": "DelegateReplyInput",
|
|
239
|
+
"outputSchemaDef": "DelegateOutput",
|
|
240
|
+
"pipeAction": "delegator_reply"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"name": "status",
|
|
244
|
+
"description": "Read the latest snapshot for an existing delegator job.",
|
|
245
|
+
"inputSchemaDef": "StatusInput",
|
|
246
|
+
"outputSchemaDef": "DelegateOutput",
|
|
247
|
+
"pipeAction": "delegator_status"
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DELEGATOR_JOB_STATUSES,
|
|
5
|
+
DELEGATOR_MODES,
|
|
6
|
+
DELEGATOR_PROVIDERS,
|
|
7
|
+
} from './contracts.mjs';
|
|
8
|
+
import { getDelegatorMcpToolDefinitions } from './tool-definitions.mjs';
|
|
9
|
+
|
|
10
|
+
function deepClone(value) {
|
|
11
|
+
if (value == null) return value;
|
|
12
|
+
return JSON.parse(JSON.stringify(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertKnown(enumValues, value, fieldName) {
|
|
16
|
+
if (value == null) return;
|
|
17
|
+
if (!enumValues.includes(value)) {
|
|
18
|
+
throw new Error(`Unsupported ${fieldName}: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DelegatorService {
|
|
23
|
+
constructor({
|
|
24
|
+
idFactory = randomUUID,
|
|
25
|
+
now = () => new Date(),
|
|
26
|
+
} = {}) {
|
|
27
|
+
this.idFactory = idFactory;
|
|
28
|
+
this.now = now;
|
|
29
|
+
this.jobs = new Map();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
listToolDefinitions() {
|
|
33
|
+
return getDelegatorMcpToolDefinitions();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
createJobSnapshot(input = {}) {
|
|
37
|
+
const timestamp = this.now().toISOString();
|
|
38
|
+
const jobId = input.job_id || this.idFactory();
|
|
39
|
+
const mode = input.mode || 'sync';
|
|
40
|
+
const providerRequested = input.provider || 'auto';
|
|
41
|
+
|
|
42
|
+
assertKnown(DELEGATOR_MODES, mode, 'mode');
|
|
43
|
+
assertKnown(DELEGATOR_PROVIDERS, providerRequested, 'provider');
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
job_id: jobId,
|
|
48
|
+
status: 'queued',
|
|
49
|
+
mode,
|
|
50
|
+
provider_requested: providerRequested,
|
|
51
|
+
provider_resolved: null,
|
|
52
|
+
agent_type: input.agent_type || 'executor',
|
|
53
|
+
transport: 'resident-pending',
|
|
54
|
+
created_at: timestamp,
|
|
55
|
+
started_at: null,
|
|
56
|
+
updated_at: timestamp,
|
|
57
|
+
completed_at: null,
|
|
58
|
+
output: '',
|
|
59
|
+
stderr: '',
|
|
60
|
+
error: '',
|
|
61
|
+
thread_id: input.thread_id || null,
|
|
62
|
+
session_key: input.session_key || null,
|
|
63
|
+
conversation_open: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
recordJob(snapshot) {
|
|
68
|
+
if (!snapshot?.job_id) {
|
|
69
|
+
throw new Error('job_id is required');
|
|
70
|
+
}
|
|
71
|
+
assertKnown(DELEGATOR_JOB_STATUSES, snapshot.status, 'status');
|
|
72
|
+
this.jobs.set(snapshot.job_id, deepClone(snapshot));
|
|
73
|
+
return this.getStatusSnapshot(snapshot.job_id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getStatusSnapshot(jobId) {
|
|
77
|
+
const snapshot = this.jobs.get(jobId);
|
|
78
|
+
return snapshot ? deepClone(snapshot) : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async delegate(_input) {
|
|
82
|
+
throw new Error('Not implemented: wire delegate to the resident worker pool and Hub pipe action.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async reply(_input) {
|
|
86
|
+
throw new Error('Not implemented: wire delegate-reply to the resident conversation handler.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async status({ job_id: jobId } = {}) {
|
|
90
|
+
const snapshot = this.getStatusSnapshot(jobId);
|
|
91
|
+
if (snapshot) {
|
|
92
|
+
return snapshot;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const timestamp = this.now().toISOString();
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
job_id: jobId || 'unknown-job',
|
|
100
|
+
status: 'failed',
|
|
101
|
+
mode: 'async',
|
|
102
|
+
provider_requested: 'auto',
|
|
103
|
+
provider_resolved: null,
|
|
104
|
+
agent_type: 'executor',
|
|
105
|
+
transport: 'resident-pending',
|
|
106
|
+
created_at: timestamp,
|
|
107
|
+
started_at: null,
|
|
108
|
+
updated_at: timestamp,
|
|
109
|
+
completed_at: null,
|
|
110
|
+
output: '',
|
|
111
|
+
stderr: '',
|
|
112
|
+
error: 'job not found',
|
|
113
|
+
thread_id: null,
|
|
114
|
+
session_key: null,
|
|
115
|
+
conversation_open: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { DELEGATOR_SCHEMA_URL } from './contracts.mjs';
|
|
4
|
+
|
|
5
|
+
let schemaBundleCache = null;
|
|
6
|
+
|
|
7
|
+
function deepClone(value) {
|
|
8
|
+
if (value == null) return value;
|
|
9
|
+
return JSON.parse(JSON.stringify(value));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function loadDelegatorSchemaBundle() {
|
|
13
|
+
if (schemaBundleCache) {
|
|
14
|
+
return schemaBundleCache;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
schemaBundleCache = JSON.parse(readFileSync(DELEGATOR_SCHEMA_URL, 'utf8'));
|
|
18
|
+
return schemaBundleCache;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getDelegatorMcpToolDefinitions() {
|
|
22
|
+
const bundle = loadDelegatorSchemaBundle();
|
|
23
|
+
const defs = bundle.$defs || {};
|
|
24
|
+
const tools = Array.isArray(bundle['x-triflux-mcp-tools'])
|
|
25
|
+
? bundle['x-triflux-mcp-tools']
|
|
26
|
+
: [];
|
|
27
|
+
|
|
28
|
+
return tools.map((tool) => ({
|
|
29
|
+
name: tool.name,
|
|
30
|
+
description: tool.description,
|
|
31
|
+
inputSchema: deepClone(defs[tool.inputSchemaDef]),
|
|
32
|
+
outputSchema: deepClone(defs[tool.outputSchemaDef]),
|
|
33
|
+
pipeAction: tool.pipeAction,
|
|
34
|
+
}));
|
|
35
|
+
}
|
package/hub/team/native.mjs
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
// SKILL.md가 인라인 프롬프트를 사용하므로, 이 모듈은 CLI(tfx multi --native)에서
|
|
7
7
|
// 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
|
|
8
8
|
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
9
13
|
const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
|
|
10
14
|
export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
|
|
11
15
|
const ROUTE_LOG_RE = /\[tfx-route\]/i;
|
|
@@ -255,6 +259,103 @@ gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통
|
|
|
255
259
|
실패 여부는 metadata.result로 구분. pane 실패 시에도 반드시 TaskUpdate + SendMessage 후 종료.`;
|
|
256
260
|
}
|
|
257
261
|
|
|
262
|
+
/**
|
|
263
|
+
* tfx-route.sh가 남긴 로컬 결과 파일을 폴링해서 완료/대기 태스크를 분리한다.
|
|
264
|
+
* SendMessage 전달 지연이 있더라도 Phase 4에서 파일 기반으로 완료를 빠르게 감지하기 위한 보조 경로다.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} teamName
|
|
267
|
+
* @param {string[]} expectedTaskIds
|
|
268
|
+
* @returns {Promise<{completed:Array<{taskId:string,result:string,summary:string}>, pending:string[]}>}
|
|
269
|
+
*/
|
|
270
|
+
export async function pollTeamResults(teamName, expectedTaskIds = []) {
|
|
271
|
+
const normalizedTaskIds = Array.from(
|
|
272
|
+
new Set(
|
|
273
|
+
(Array.isArray(expectedTaskIds) ? expectedTaskIds : [])
|
|
274
|
+
.map((taskId) => String(taskId || "").trim())
|
|
275
|
+
.filter(Boolean),
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (!normalizedTaskIds.length) {
|
|
280
|
+
return { completed: [], pending: [] };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const normalizedTeamName = String(teamName || "").trim();
|
|
284
|
+
if (!normalizedTeamName) {
|
|
285
|
+
return { completed: [], pending: normalizedTaskIds };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const resultDir = path.join(os.homedir(), ".claude", "tfx-results", normalizedTeamName);
|
|
289
|
+
|
|
290
|
+
let entries;
|
|
291
|
+
try {
|
|
292
|
+
entries = await fs.readdir(resultDir, { withFileTypes: true });
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error && error.code === "ENOENT") {
|
|
295
|
+
return { completed: [], pending: normalizedTaskIds };
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const availableFiles = new Set(
|
|
301
|
+
entries
|
|
302
|
+
.filter((entry) => entry.isFile())
|
|
303
|
+
.map((entry) => entry.name),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const completedCandidates = await Promise.all(
|
|
307
|
+
normalizedTaskIds.map(async (taskId) => {
|
|
308
|
+
const fileName = `${taskId}.json`;
|
|
309
|
+
if (!availableFiles.has(fileName)) return null;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const raw = await fs.readFile(path.join(resultDir, fileName), "utf8");
|
|
313
|
+
const parsed = JSON.parse(raw);
|
|
314
|
+
return {
|
|
315
|
+
taskId,
|
|
316
|
+
result: typeof parsed?.result === "string" ? parsed.result : "failed",
|
|
317
|
+
summary: typeof parsed?.summary === "string" ? parsed.summary : "",
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error && error.code === "ENOENT") return null;
|
|
321
|
+
return {
|
|
322
|
+
taskId,
|
|
323
|
+
result: "failed",
|
|
324
|
+
summary: "결과 파일 파싱 실패",
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const completed = completedCandidates.filter(Boolean);
|
|
331
|
+
const completedTaskIds = new Set(completed.map((item) => item.taskId));
|
|
332
|
+
const pending = normalizedTaskIds.filter((taskId) => !completedTaskIds.has(taskId));
|
|
333
|
+
|
|
334
|
+
return { completed, pending };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 폴링 결과를 진행률 한 줄 요약으로 바꾼다.
|
|
339
|
+
*
|
|
340
|
+
* @param {{completed?:Array<{taskId:string,result:string}>, pending?:string[]}} pollResult
|
|
341
|
+
* @returns {string}
|
|
342
|
+
*/
|
|
343
|
+
export function formatPollReport(pollResult = {}) {
|
|
344
|
+
const completed = Array.isArray(pollResult.completed) ? pollResult.completed : [];
|
|
345
|
+
const pending = Array.isArray(pollResult.pending) ? pollResult.pending : [];
|
|
346
|
+
const total = completed.length + pending.length;
|
|
347
|
+
|
|
348
|
+
if (total === 0) return "0/0 완료";
|
|
349
|
+
|
|
350
|
+
const detail = completed
|
|
351
|
+
.map(({ taskId, result }) => `${taskId} ${result || "unknown"}`)
|
|
352
|
+
.join(", ");
|
|
353
|
+
|
|
354
|
+
return detail
|
|
355
|
+
? `${completed.length}/${total} 완료 (${detail})`
|
|
356
|
+
: `${completed.length}/${total} 완료`;
|
|
357
|
+
}
|
|
358
|
+
|
|
258
359
|
/**
|
|
259
360
|
* 팀 이름 생성 (타임스탬프 기반)
|
|
260
361
|
* @returns {string}
|