@triflux/core 10.1.0 → 10.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +191 -31
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +262 -34
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +1051 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +22 -21
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/schema/delegator-tools.schema.json +1 -1
  35. package/hub/delegator/service.mjs +93 -64
  36. package/hub/delegator/tool-definitions.mjs +35 -35
  37. package/hub/dynamic-routing-engine.mjs +511 -0
  38. package/hub/fullcycle.mjs +34 -18
  39. package/hub/gemini-adapter.mjs +82 -108
  40. package/hub/hitl.mjs +202 -143
  41. package/hub/intent.mjs +167 -38
  42. package/hub/lib/bash-path.mjs +73 -0
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/env-detect.mjs +145 -0
  45. package/hub/lib/hosts-compat.mjs +319 -0
  46. package/hub/lib/mcp-response-cache.mjs +239 -0
  47. package/hub/lib/memory-store.mjs +285 -111
  48. package/hub/lib/path-utils.mjs +165 -0
  49. package/hub/lib/phase-manager.mjs +493 -0
  50. package/hub/lib/process-utils.mjs +797 -104
  51. package/hub/lib/prompt-tmp.mjs +33 -0
  52. package/hub/lib/risk-tier.mjs +53 -0
  53. package/hub/lib/spawn-trace.mjs +561 -0
  54. package/hub/lib/ssh-command.mjs +44 -24
  55. package/hub/lib/ssh-retry.mjs +5 -1
  56. package/hub/lib/staged-file-detect.mjs +43 -0
  57. package/hub/lib/state-snapshot.mjs +293 -0
  58. package/hub/lib/tfx-route-args.mjs +309 -0
  59. package/hub/lib/timeline-adapter.mjs +190 -0
  60. package/hub/lib/trace-recorder.mjs +153 -0
  61. package/hub/lib/uuidv7.mjs +4 -3
  62. package/hub/memory-doctor.mjs +266 -106
  63. package/hub/middleware/quota-middleware.mjs +70 -0
  64. package/hub/middleware/request-logger.mjs +61 -34
  65. package/hub/paths.mjs +9 -9
  66. package/hub/pipeline/gates/confidence.mjs +34 -15
  67. package/hub/pipeline/gates/consensus.mjs +27 -15
  68. package/hub/pipeline/gates/index.mjs +7 -3
  69. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  70. package/hub/pipeline/index.mjs +77 -42
  71. package/hub/pipeline/state.mjs +10 -10
  72. package/hub/pipeline/transitions.mjs +40 -23
  73. package/hub/platform.mjs +65 -48
  74. package/hub/quality/deslop.mjs +70 -49
  75. package/hub/reflexion.mjs +87 -13
  76. package/hub/research.mjs +33 -25
  77. package/hub/router.mjs +941 -791
  78. package/hub/routing/complexity.mjs +132 -29
  79. package/hub/routing/index.mjs +17 -12
  80. package/hub/routing/q-learning.mjs +78 -30
  81. package/hub/routing-snapshot.mjs +261 -0
  82. package/hub/session-fingerprint.mjs +127 -61
  83. package/hub/state.mjs +86 -44
  84. package/hub/team/claude-daemon-control.mjs +1130 -0
  85. package/hub/team/retry-state-machine.mjs +336 -0
  86. package/hub/token-mode.mjs +72 -71
  87. package/hub/workers/worker-utils.mjs +23 -17
  88. package/hud/colors.mjs +27 -9
  89. package/hud/constants.mjs +190 -27
  90. package/hud/context-monitor.mjs +133 -44
  91. package/hud/hud-qos-status.mjs +157 -50
  92. package/hud/mission-board.mjs +56 -0
  93. package/hud/providers/claude.mjs +398 -114
  94. package/hud/providers/codex.mjs +96 -21
  95. package/hud/providers/gemini.mjs +210 -52
  96. package/hud/renderers.mjs +440 -127
  97. package/hud/terminal.mjs +101 -31
  98. package/hud/utils.mjs +81 -39
  99. package/mesh/index.mjs +69 -0
  100. package/mesh/mesh-budget.mjs +137 -0
  101. package/mesh/mesh-heartbeat.mjs +100 -0
  102. package/mesh/mesh-protocol.mjs +96 -0
  103. package/mesh/mesh-queue.mjs +163 -0
  104. package/mesh/mesh-registry.mjs +78 -0
  105. package/mesh/mesh-router.mjs +77 -0
  106. package/package.json +5 -1
  107. package/scripts/lib/agent-json.mjs +27 -0
  108. package/scripts/lib/async.mjs +174 -0
  109. package/scripts/lib/claudemd-scanner.mjs +6 -1
  110. package/scripts/lib/cli-agy.mjs +62 -0
  111. package/scripts/lib/cli-claude.mjs +78 -0
  112. package/scripts/lib/cli-codex.mjs +199 -0
  113. package/scripts/lib/cli-gemini.mjs +37 -0
  114. package/scripts/lib/codex-recovery.sh +50 -0
  115. package/scripts/lib/context.mjs +3 -3
  116. package/scripts/lib/cross-review-utils.mjs +13 -5
  117. package/scripts/lib/doctor-env-checks.mjs +121 -0
  118. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  119. package/scripts/lib/env-probe.mjs +58 -30
  120. package/scripts/lib/env.mjs +14 -0
  121. package/scripts/lib/gemini-profiles.mjs +44 -10
  122. package/scripts/lib/handoff.mjs +33 -17
  123. package/scripts/lib/hook-utils.mjs +8 -6
  124. package/scripts/lib/hub.mjs +48 -0
  125. package/scripts/lib/keyword-rules.mjs +49 -20
  126. package/scripts/lib/logger.mjs +24 -24
  127. package/scripts/lib/mcp-filter.mjs +391 -247
  128. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  129. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  130. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  131. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  132. package/scripts/lib/mcp-health.mjs +773 -0
  133. package/scripts/lib/mcp-manifest.mjs +24 -14
  134. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  135. package/scripts/lib/pid.mjs +63 -0
  136. package/scripts/lib/process-utils.mjs +26 -0
  137. package/scripts/lib/psmux-info.mjs +117 -12
  138. package/scripts/lib/quota.mjs +47 -0
  139. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  140. package/scripts/lib/skill-state.mjs +220 -0
  141. package/scripts/lib/skill-template.mjs +32 -7
  142. package/scripts/lib/team.mjs +75 -0
  143. package/scripts/lib/timeout.mjs +66 -0
  144. package/scripts/lib/tmp.mjs +26 -0
  145. package/scripts/lib/toml.mjs +113 -0
@@ -1,41 +1,45 @@
1
- // hub/gemini-adapter.mjs — Gemini CLI 방어 계층
2
- // codex-adapter.mjs와 동일 패턴, cli-adapter-base 공통 인터페이스 사용
1
+ // hub/gemini-adapter.mjs — legacy Gemini adapter compatibility layer
2
+ // The "gemini" route is retained as an alias, but execution is Antigravity/agy.
3
3
 
4
- import { mkdirSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { tmpdir } from 'node:os';
7
-
8
- import { withRetry } from './workers/worker-utils.mjs';
9
- import { whichCommandAsync } from './platform.mjs';
4
+ import { mkdirSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
10
7
  import {
11
- createCircuitBreaker,
12
- createResult,
13
- appendWarnings,
8
+ executeWithCircuitBroker,
14
9
  normalizePathForShell,
15
- shellQuote,
16
10
  runProcess,
17
- } from './cli-adapter-base.mjs';
11
+ } from "./cli-adapter-base.mjs";
12
+ import { whichCommandAsync } from "./platform.mjs";
18
13
 
19
- const breaker = createCircuitBreaker();
14
+ function shellSingleQuote(value) {
15
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
16
+ }
20
17
 
21
- // ── Gemini-specific stall inference ─────────────────────────────
18
+ // ── Antigravity stall inference ─────────────────────────────────
22
19
 
23
20
  function inferStallMode(stdout, stderr) {
24
21
  const text = `${stdout}\n${stderr}`.toLowerCase();
25
- if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text)) return 'rate_limited';
26
- if (/(unauthorized|forbidden|auth|login|token|credential|api.?key)/u.test(text)) return 'auth_stall';
27
- if (/\bmcp\b|playwright|tavily|brave|sequential|server/u.test(text)) return 'mcp_stall';
28
- return 'timeout';
22
+ if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text))
23
+ return "rate_limited";
24
+ if (
25
+ /(unauthorized|forbidden|auth|login|token|credential|api.?key)/u.test(text)
26
+ )
27
+ return "auth_stall";
28
+ if (/\bmcp\b|playwright|tavily|brave|sequential|server/u.test(text))
29
+ return "mcp_stall";
30
+ return "timeout";
29
31
  }
30
32
 
31
33
  // ── Preflight ───────────────────────────────────────────────────
32
34
 
33
35
  async function runPreflight(opts = {}) {
34
- const geminiPath = await whichCommandAsync('gemini');
35
- if (!geminiPath) {
36
+ const agyPath = await whichCommandAsync("agy");
37
+ if (!agyPath) {
36
38
  return {
37
- geminiPath: null,
38
- warnings: ['Gemini CLI not found. Install Gemini and ensure `gemini` is available on PATH.'],
39
+ agyPath: null,
40
+ warnings: [
41
+ "Antigravity CLI not found. Install Antigravity and ensure `agy` is available on PATH.",
42
+ ],
39
43
  excludeMcpServers: [],
40
44
  ok: false,
41
45
  };
@@ -45,38 +49,32 @@ async function runPreflight(opts = {}) {
45
49
  const excludeMcpServers = [];
46
50
 
47
51
  for (const name of Array.isArray(opts.mcpServers) ? opts.mcpServers : []) {
48
- const server = String(name ?? '').trim();
52
+ const server = String(name ?? "").trim();
49
53
  if (!server) continue;
50
- // Gemini MCP health는 best-effort: 실행 시점에 --allowed-mcp-server-names로 필터링
51
- // 사전 probe는 수행하지 않음 (gemini가 자체적으로 graceful degrade)
54
+ // Antigravity MCP health는 best-effort: 실행 시점 probe는 수행하지 않음.
52
55
  }
53
56
 
54
- return { geminiPath, warnings, excludeMcpServers, ok: true };
57
+ return { agyPath, warnings, excludeMcpServers, ok: true };
55
58
  }
56
59
 
57
60
  // ── Command building ────────────────────────────────────────────
58
61
 
59
62
  function buildGeminiCommand(prompt, resultFile, opts = {}) {
60
- const parts = ['gemini'];
61
-
62
- if (opts.model) parts.push('--model', shellQuote(opts.model));
63
- parts.push('--yolo');
64
-
65
- const allowed = Array.isArray(opts.allowedMcpServers) ? opts.allowedMcpServers : [];
66
- const excluded = Array.isArray(opts.excludeMcpServers) ? opts.excludeMcpServers : [];
67
- const filtered = allowed.filter((name) => !excluded.includes(name));
68
- if (filtered.length) {
69
- parts.push('--allowed-mcp-server-names', ...filtered.map((n) => shellQuote(n)));
70
- }
71
-
72
- parts.push('--prompt', shellQuote(prompt));
73
- parts.push('--output-format', 'text');
63
+ const parts = [
64
+ "printf",
65
+ "%s",
66
+ shellSingleQuote(prompt),
67
+ "|",
68
+ "agy",
69
+ "--print",
70
+ "--dangerously-skip-permissions",
71
+ ];
74
72
 
75
73
  if (resultFile) {
76
- return `${parts.join(' ')} > ${shellQuote(normalizePathForShell(resultFile))} 2>${shellQuote(normalizePathForShell(resultFile + '.err'))}`;
74
+ return `${parts.join(" ")} > ${shellSingleQuote(normalizePathForShell(resultFile))} 2>${shellSingleQuote(normalizePathForShell(resultFile + ".err"))}`;
77
75
  }
78
76
 
79
- return parts.join(' ');
77
+ return parts.join(" ");
80
78
  }
81
79
 
82
80
  function buildAttempts(opts, preflight) {
@@ -84,20 +82,19 @@ function buildAttempts(opts, preflight) {
84
82
  const base = {
85
83
  timeout,
86
84
  model: opts.model,
87
- allowedMcpServers: Array.isArray(opts.mcpServers) ? [...opts.mcpServers] : [],
85
+ allowedMcpServers: Array.isArray(opts.mcpServers)
86
+ ? [...opts.mcpServers]
87
+ : [],
88
88
  excludeMcpServers: [...(preflight.excludeMcpServers || [])],
89
89
  };
90
90
  if (opts.retryOnFail === false) return [base];
91
- return [
92
- base,
93
- { ...base, timeout: timeout * 2, allowedMcpServers: [] },
94
- ];
91
+ return [base, { ...base, timeout: timeout * 2, allowedMcpServers: [] }];
95
92
  }
96
93
 
97
94
  // ── Public: buildExecArgs ───────────────────────────────────────
98
95
 
99
96
  export function buildExecArgs(opts = {}) {
100
- const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
97
+ const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
101
98
  return buildGeminiCommand(prompt, opts.resultFile || null, {
102
99
  model: opts.model,
103
100
  allowedMcpServers: opts.mcpServers,
@@ -106,75 +103,52 @@ export function buildExecArgs(opts = {}) {
106
103
 
107
104
  // ── Execution ───────────────────────────────────────────────────
108
105
 
109
- async function runGemini(prompt, workdir, preflight, attempt) {
110
- const dir = join(tmpdir(), 'triflux-gemini-exec');
106
+ async function runGemini(prompt, workdir, preflight, attempt, lease) {
107
+ const dir = join(tmpdir(), "triflux-antigravity-exec");
111
108
  mkdirSync(dir, { recursive: true });
112
- const resultFile = join(dir, `gemini-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
109
+ const resultFile = join(
110
+ dir,
111
+ `agy-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`,
112
+ );
113
113
  const command = buildGeminiCommand(prompt, resultFile, {
114
114
  model: attempt.model,
115
115
  allowedMcpServers: attempt.allowedMcpServers,
116
116
  excludeMcpServers: attempt.excludeMcpServers,
117
117
  });
118
- return runProcess(command, workdir, attempt.timeout, { resultFile, inferStallMode });
118
+ // PRD A1: lease.env 가 있으면 spawn env 에 적용 (예: GOOGLE_API_KEY 등 account-specific
119
+ // 환경변수). lease 가 null 이면 default 동작 유지 (부모 env inherit).
120
+ const spawnEnv =
121
+ lease?.env && typeof lease.env === "object"
122
+ ? { ...process.env, ...lease.env }
123
+ : undefined;
124
+ return runProcess(command, workdir, attempt.timeout, {
125
+ resultFile,
126
+ inferStallMode,
127
+ spawnEnv,
128
+ });
119
129
  }
120
130
 
121
131
  // ── Public API ──────────────────────────────────────────────────
122
132
 
123
- export function getCircuitState(now) {
124
- return breaker.getState(now);
133
+ export async function getCircuitState() {
134
+ const brokerMod = await import("./account-broker.mjs");
135
+ if (!brokerMod.broker) return { state: "closed", failures: [] };
136
+ const snap = brokerMod.broker
137
+ .snapshot()
138
+ .filter((a) => a.provider === "gemini");
139
+ return snap.length
140
+ ? { state: snap[0].circuitState, accounts: snap }
141
+ : { state: "closed", failures: [] };
125
142
  }
126
143
 
127
- export async function execute(opts = {}) {
128
- const entry = breaker.canExecute();
129
- if (!entry.allowed) {
130
- return createResult(false, { fellBack: true, failureMode: 'circuit_open' });
131
- }
132
-
133
- const preflight = await runPreflight({ mcpServers: opts.mcpServers });
134
- if (!preflight.ok) {
135
- breaker.clearTrial();
136
- breaker.recordFailure(entry.halfOpen);
137
- return createResult(false, {
138
- stderr: appendWarnings('', preflight.warnings),
139
- fellBack: opts.fallbackToClaude !== false,
140
- failureMode: 'crash',
141
- });
142
- }
143
-
144
- const attempts = buildAttempts(opts, preflight);
145
- let attemptIndex = 0;
146
- let lastResult = createResult(false);
147
-
148
- try {
149
- lastResult = await withRetry(async () => {
150
- const result = await runGemini(opts.prompt || '', opts.workdir || process.cwd(), preflight, attempts[attemptIndex]);
151
- const current = { ...result, stderr: appendWarnings(result.stderr, preflight.warnings), retried: attemptIndex > 0 };
152
- const canRetry = !current.ok && attemptIndex < attempts.length - 1;
153
- attemptIndex += 1;
154
- if (!canRetry) return current;
155
- const error = new Error('retry');
156
- error.retryable = true;
157
- error.result = current;
158
- throw error;
159
- }, {
160
- maxAttempts: attempts.length,
161
- baseDelayMs: 250,
162
- maxDelayMs: 750,
163
- shouldRetry: (error) => error?.retryable === true,
164
- });
165
- } catch (error) {
166
- lastResult = error?.result || createResult(false, { stderr: String(error?.message || error) });
167
- }
168
-
169
- if (lastResult.ok) {
170
- breaker.reset();
171
- return lastResult;
172
- }
173
-
174
- breaker.recordFailure(entry.halfOpen);
175
- return {
176
- ...lastResult,
177
- retried: attempts.length > 1,
178
- fellBack: opts.fallbackToClaude !== false,
179
- };
144
+ export function execute(opts = {}) {
145
+ return executeWithCircuitBroker({
146
+ // Account-broker still stores this compatibility bucket as "gemini";
147
+ // execution itself is routed to agy above.
148
+ provider: "gemini",
149
+ runFn: runGemini,
150
+ preflightFn: (o) => runPreflight({ mcpServers: o.mcpServers }),
151
+ buildAttemptsFn: buildAttempts,
152
+ opts,
153
+ });
180
154
  }
package/hub/hitl.mjs CHANGED
@@ -1,143 +1,202 @@
1
- // hub/hitl.mjs — Human-in-the-Loop 매니저
2
- // 사용자 입력 요청/응답, 타임아웃 자동 처리
3
-
4
- /**
5
- * HITL 매니저 생성
6
- * @param {object} store — createStore() 반환 객체
7
- * @param {object} router — createRouter() 반환 객체
8
- */
9
- export function createHitlManager(store, router = null) {
10
- function forwardHumanResponse({ requesterAgent, requestId, action, content, submittedBy, correlationId, traceId, priority }) {
11
- if (!router?.handlePublish) {
12
- throw new Error('router.handlePublish is required for HITL forwarding');
13
- }
14
- return router.handlePublish({
15
- from: 'hub:hitl',
16
- to: requesterAgent,
17
- topic: 'human.response',
18
- priority,
19
- ttl_ms: 300000,
20
- payload: { request_id: requestId, action, content, submitted_by: submittedBy },
21
- correlation_id: correlationId,
22
- trace_id: traceId,
23
- message_type: 'human_response',
24
- });
25
- }
26
-
27
- return {
28
- /**
29
- * 사용자에게 입력 요청 생성
30
- * 터미널에 알림 출력 후 pending 상태로 저장
31
- */
32
- requestHumanInput({
33
- requester_agent, kind, prompt, requested_schema = {},
34
- deadline_ms, default_action, channel_preference = 'terminal',
35
- correlation_id, trace_id,
36
- }) {
37
- const result = store.insertHumanRequest({
38
- requester_agent, kind, prompt, requested_schema,
39
- deadline_ms, default_action,
40
- correlation_id, trace_id,
41
- });
42
-
43
- // 터미널 알림 (stderr — stdout은 MCP 용)
44
- const kindLabel = { captcha: 'CAPTCHA', approval: '승인', credential: '자격증명', choice: '선택', text: '텍스트' };
45
- process.stderr.write(
46
- `\n[tfx-hub] 사용자 입력 요청 (${kindLabel[kind] || kind})\n` +
47
- ` 요청자: ${requester_agent}\n` +
48
- ` 내용: ${prompt}\n` +
49
- ` ID: ${result.request_id}\n` +
50
- ` 제한: ${Math.round(deadline_ms / 1000)}초\n\n`,
51
- );
52
-
53
- return { ok: true, data: result };
54
- },
55
-
56
- /**
57
- * 사용자 입력 응답 제출
58
- * 유효성 검증 → 상태 업데이트 → 요청자에게 응답 메시지 전달
59
- */
60
- submitHumanInput({ request_id, action, content = null, submitted_by = 'human' }) {
61
- // 요청 조회
62
- const hr = store.getHumanRequest(request_id);
63
- if (!hr) {
64
- return { ok: false, error: { code: 'NOT_FOUND', message: `요청 없음: ${request_id}` } };
65
- }
66
- if (hr.state !== 'pending') {
67
- return { ok: false, error: { code: 'ALREADY_HANDLED', message: `이미 처리됨: ${hr.state}` } };
68
- }
69
-
70
- // 상태 매핑
71
- const stateMap = { accept: 'accepted', decline: 'declined', cancel: 'cancelled' };
72
- const newState = stateMap[action];
73
- if (!newState) {
74
- return { ok: false, error: { code: 'INVALID_ACTION', message: `잘못된 action: ${action}` } };
75
- }
76
-
77
- // DB 업데이트
78
- store.updateHumanRequest(request_id, newState, content);
79
-
80
- // 요청자에게 응답 메시지 전달
81
- let forwardedMessageId = null;
82
- if (action === 'accept' || action === 'decline') {
83
- const published = forwardHumanResponse({
84
- requesterAgent: hr.requester_agent,
85
- requestId: request_id,
86
- action,
87
- content,
88
- submittedBy: submitted_by,
89
- correlationId: hr.correlation_id,
90
- traceId: hr.trace_id,
91
- priority: 7,
92
- });
93
- forwardedMessageId = published.data?.message_id || null;
94
- }
95
-
96
- return {
97
- ok: true,
98
- data: { request_id, new_state: newState, forwarded_message_id: forwardedMessageId },
99
- };
100
- },
101
-
102
- /**
103
- * 만료된 요청 자동 처리
104
- * deadline 초과 시 default_action 적용
105
- */
106
- checkTimeouts() {
107
- const pending = store.getPendingHumanRequests();
108
- const now = Date.now();
109
- const expired = pending.filter(hr => hr.deadline_ms <= now);
110
- if (!expired.length) return 0;
111
-
112
- const expireRequests = () => {
113
- for (const hr of expired) {
114
- store.updateHumanRequest(hr.request_id, 'timed_out', null);
115
- if (hr.default_action === 'timeout_continue') {
116
- forwardHumanResponse({
117
- requesterAgent: hr.requester_agent,
118
- requestId: hr.request_id,
119
- action: 'timeout_continue',
120
- content: null,
121
- submittedBy: 'system',
122
- correlationId: hr.correlation_id,
123
- traceId: hr.trace_id,
124
- priority: 5,
125
- });
126
- }
127
- }
128
- return expired.length;
129
- };
130
-
131
- const processExpired = store.db?.transaction
132
- ? store.db.transaction(expireRequests)
133
- : expireRequests;
134
-
135
- return processExpired();
136
- },
137
-
138
- /** 대기 중인 요청 목록 */
139
- getPendingRequests() {
140
- return store.getPendingHumanRequests();
141
- },
142
- };
143
- }
1
+ // hub/hitl.mjs — Human-in-the-Loop 매니저
2
+ // 사용자 입력 요청/응답, 타임아웃 자동 처리
3
+
4
+ /**
5
+ * HITL 매니저 생성
6
+ * @param {object} store — createStore() 반환 객체
7
+ * @param {object} router — createRouter() 반환 객체
8
+ */
9
+ export function createHitlManager(store, router = null) {
10
+ function forwardHumanResponse({
11
+ requesterAgent,
12
+ requestId,
13
+ action,
14
+ content,
15
+ submittedBy,
16
+ correlationId,
17
+ traceId,
18
+ priority,
19
+ }) {
20
+ if (!router?.handlePublish) {
21
+ throw new Error("router.handlePublish is required for HITL forwarding");
22
+ }
23
+ return router.handlePublish({
24
+ from: "hub:hitl",
25
+ to: requesterAgent,
26
+ topic: "human.response",
27
+ priority,
28
+ ttl_ms: 300000,
29
+ payload: {
30
+ request_id: requestId,
31
+ action,
32
+ content,
33
+ submitted_by: submittedBy,
34
+ },
35
+ correlation_id: correlationId,
36
+ trace_id: traceId,
37
+ message_type: "human_response",
38
+ });
39
+ }
40
+
41
+ return {
42
+ /**
43
+ * 사용자에게 입력 요청 생성
44
+ * 터미널에 알림 출력 후 pending 상태로 저장
45
+ */
46
+ requestHumanInput({
47
+ requester_agent,
48
+ kind,
49
+ prompt,
50
+ requested_schema = {},
51
+ deadline_ms,
52
+ default_action,
53
+ channel_preference = "terminal",
54
+ correlation_id,
55
+ trace_id,
56
+ }) {
57
+ const result = store.insertHumanRequest({
58
+ requester_agent,
59
+ kind,
60
+ prompt,
61
+ requested_schema,
62
+ deadline_ms,
63
+ default_action,
64
+ correlation_id,
65
+ trace_id,
66
+ });
67
+
68
+ // 터미널 알림 (stderr — stdout은 MCP 용)
69
+ const kindLabel = {
70
+ captcha: "CAPTCHA",
71
+ approval: "승인",
72
+ credential: "자격증명",
73
+ choice: "선택",
74
+ text: "텍스트",
75
+ };
76
+ process.stderr.write(
77
+ `\n[tfx-hub] 사용자 입력 요청 (${kindLabel[kind] || kind})\n` +
78
+ ` 요청자: ${requester_agent}\n` +
79
+ ` 내용: ${prompt}\n` +
80
+ ` ID: ${result.request_id}\n` +
81
+ ` 제한: ${Math.round(deadline_ms / 1000)}초\n\n`,
82
+ );
83
+
84
+ return { ok: true, data: result };
85
+ },
86
+
87
+ /**
88
+ * 사용자 입력 응답 제출
89
+ * 유효성 검증 → 상태 업데이트 → 요청자에게 응답 메시지 전달
90
+ */
91
+ submitHumanInput({
92
+ request_id,
93
+ action,
94
+ content = null,
95
+ submitted_by = "human",
96
+ }) {
97
+ // 요청 조회
98
+ const hr = store.getHumanRequest(request_id);
99
+ if (!hr) {
100
+ return {
101
+ ok: false,
102
+ error: { code: "NOT_FOUND", message: `요청 없음: ${request_id}` },
103
+ };
104
+ }
105
+ if (hr.state !== "pending") {
106
+ return {
107
+ ok: false,
108
+ error: {
109
+ code: "ALREADY_HANDLED",
110
+ message: `이미 처리됨: ${hr.state}`,
111
+ },
112
+ };
113
+ }
114
+
115
+ // 상태 매핑
116
+ const stateMap = {
117
+ accept: "accepted",
118
+ decline: "declined",
119
+ cancel: "cancelled",
120
+ };
121
+ const newState = stateMap[action];
122
+ if (!newState) {
123
+ return {
124
+ ok: false,
125
+ error: {
126
+ code: "INVALID_ACTION",
127
+ message: `잘못된 action: ${action}`,
128
+ },
129
+ };
130
+ }
131
+
132
+ // DB 업데이트
133
+ store.updateHumanRequest(request_id, newState, content);
134
+
135
+ // 요청자에게 응답 메시지 전달
136
+ let forwardedMessageId = null;
137
+ if (action === "accept" || action === "decline") {
138
+ const published = forwardHumanResponse({
139
+ requesterAgent: hr.requester_agent,
140
+ requestId: request_id,
141
+ action,
142
+ content,
143
+ submittedBy: submitted_by,
144
+ correlationId: hr.correlation_id,
145
+ traceId: hr.trace_id,
146
+ priority: 7,
147
+ });
148
+ forwardedMessageId = published.data?.message_id || null;
149
+ }
150
+
151
+ return {
152
+ ok: true,
153
+ data: {
154
+ request_id,
155
+ new_state: newState,
156
+ forwarded_message_id: forwardedMessageId,
157
+ },
158
+ };
159
+ },
160
+
161
+ /**
162
+ * 만료된 요청 자동 처리
163
+ * deadline 초과 시 default_action 적용
164
+ */
165
+ checkTimeouts() {
166
+ const pending = store.getPendingHumanRequests();
167
+ const now = Date.now();
168
+ const expired = pending.filter((hr) => hr.deadline_ms <= now);
169
+ if (!expired.length) return 0;
170
+
171
+ const expireRequests = () => {
172
+ for (const hr of expired) {
173
+ store.updateHumanRequest(hr.request_id, "timed_out", null);
174
+ if (hr.default_action === "timeout_continue") {
175
+ forwardHumanResponse({
176
+ requesterAgent: hr.requester_agent,
177
+ requestId: hr.request_id,
178
+ action: "timeout_continue",
179
+ content: null,
180
+ submittedBy: "system",
181
+ correlationId: hr.correlation_id,
182
+ traceId: hr.trace_id,
183
+ priority: 5,
184
+ });
185
+ }
186
+ }
187
+ return expired.length;
188
+ };
189
+
190
+ const processExpired = store.db?.transaction
191
+ ? store.db.transaction(expireRequests)
192
+ : expireRequests;
193
+
194
+ return processExpired();
195
+ },
196
+
197
+ /** 대기 중인 요청 목록 */
198
+ getPendingRequests() {
199
+ return store.getPendingHumanRequests();
200
+ },
201
+ };
202
+ }