@triflux/core 10.1.0 → 10.25.1
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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
package/hub/gemini-adapter.mjs
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
// hub/gemini-adapter.mjs — Gemini CLI 방어 계층
|
|
2
2
|
// codex-adapter.mjs와 동일 패턴, cli-adapter-base 공통 인터페이스 사용
|
|
3
3
|
|
|
4
|
-
import { mkdirSync } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
-
|
|
12
|
-
createResult,
|
|
13
|
-
appendWarnings,
|
|
8
|
+
executeWithCircuitBroker,
|
|
14
9
|
normalizePathForShell,
|
|
15
|
-
shellQuote,
|
|
16
10
|
runProcess,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
shellQuote,
|
|
12
|
+
} from "./cli-adapter-base.mjs";
|
|
13
|
+
import { whichCommandAsync } from "./platform.mjs";
|
|
20
14
|
|
|
21
15
|
// ── Gemini-specific stall inference ─────────────────────────────
|
|
22
16
|
|
|
23
17
|
function inferStallMode(stdout, stderr) {
|
|
24
18
|
const text = `${stdout}\n${stderr}`.toLowerCase();
|
|
25
|
-
if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text))
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
19
|
+
if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text))
|
|
20
|
+
return "rate_limited";
|
|
21
|
+
if (
|
|
22
|
+
/(unauthorized|forbidden|auth|login|token|credential|api.?key)/u.test(text)
|
|
23
|
+
)
|
|
24
|
+
return "auth_stall";
|
|
25
|
+
if (/\bmcp\b|playwright|tavily|brave|sequential|server/u.test(text))
|
|
26
|
+
return "mcp_stall";
|
|
27
|
+
return "timeout";
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
// ── Preflight ───────────────────────────────────────────────────
|
|
32
31
|
|
|
33
32
|
async function runPreflight(opts = {}) {
|
|
34
|
-
const geminiPath = await whichCommandAsync(
|
|
33
|
+
const geminiPath = await whichCommandAsync("gemini");
|
|
35
34
|
if (!geminiPath) {
|
|
36
35
|
return {
|
|
37
36
|
geminiPath: null,
|
|
38
|
-
warnings: [
|
|
37
|
+
warnings: [
|
|
38
|
+
"Gemini CLI not found. Install Gemini and ensure `gemini` is available on PATH.",
|
|
39
|
+
],
|
|
39
40
|
excludeMcpServers: [],
|
|
40
41
|
ok: false,
|
|
41
42
|
};
|
|
@@ -45,7 +46,7 @@ async function runPreflight(opts = {}) {
|
|
|
45
46
|
const excludeMcpServers = [];
|
|
46
47
|
|
|
47
48
|
for (const name of Array.isArray(opts.mcpServers) ? opts.mcpServers : []) {
|
|
48
|
-
const server = String(name ??
|
|
49
|
+
const server = String(name ?? "").trim();
|
|
49
50
|
if (!server) continue;
|
|
50
51
|
// Gemini MCP health는 best-effort: 실행 시점에 --allowed-mcp-server-names로 필터링
|
|
51
52
|
// 사전 probe는 수행하지 않음 (gemini가 자체적으로 graceful degrade)
|
|
@@ -57,26 +58,33 @@ async function runPreflight(opts = {}) {
|
|
|
57
58
|
// ── Command building ────────────────────────────────────────────
|
|
58
59
|
|
|
59
60
|
function buildGeminiCommand(prompt, resultFile, opts = {}) {
|
|
60
|
-
const parts = [
|
|
61
|
+
const parts = ["gemini"];
|
|
61
62
|
|
|
62
|
-
if (opts.model) parts.push(
|
|
63
|
-
parts.push(
|
|
63
|
+
if (opts.model) parts.push("--model", shellQuote(opts.model));
|
|
64
|
+
parts.push("--yolo");
|
|
64
65
|
|
|
65
|
-
const allowed = Array.isArray(opts.allowedMcpServers)
|
|
66
|
-
|
|
66
|
+
const allowed = Array.isArray(opts.allowedMcpServers)
|
|
67
|
+
? opts.allowedMcpServers
|
|
68
|
+
: [];
|
|
69
|
+
const excluded = Array.isArray(opts.excludeMcpServers)
|
|
70
|
+
? opts.excludeMcpServers
|
|
71
|
+
: [];
|
|
67
72
|
const filtered = allowed.filter((name) => !excluded.includes(name));
|
|
68
73
|
if (filtered.length) {
|
|
69
|
-
parts.push(
|
|
74
|
+
parts.push(
|
|
75
|
+
"--allowed-mcp-server-names",
|
|
76
|
+
...filtered.map((n) => shellQuote(n)),
|
|
77
|
+
);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
parts.push(
|
|
73
|
-
parts.push(
|
|
80
|
+
parts.push("--prompt", shellQuote(prompt));
|
|
81
|
+
parts.push("--output-format", "text");
|
|
74
82
|
|
|
75
83
|
if (resultFile) {
|
|
76
|
-
return `${parts.join(
|
|
84
|
+
return `${parts.join(" ")} > ${shellQuote(normalizePathForShell(resultFile))} 2>${shellQuote(normalizePathForShell(resultFile + ".err"))}`;
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
return parts.join(
|
|
87
|
+
return parts.join(" ");
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
function buildAttempts(opts, preflight) {
|
|
@@ -84,20 +92,19 @@ function buildAttempts(opts, preflight) {
|
|
|
84
92
|
const base = {
|
|
85
93
|
timeout,
|
|
86
94
|
model: opts.model,
|
|
87
|
-
allowedMcpServers: Array.isArray(opts.mcpServers)
|
|
95
|
+
allowedMcpServers: Array.isArray(opts.mcpServers)
|
|
96
|
+
? [...opts.mcpServers]
|
|
97
|
+
: [],
|
|
88
98
|
excludeMcpServers: [...(preflight.excludeMcpServers || [])],
|
|
89
99
|
};
|
|
90
100
|
if (opts.retryOnFail === false) return [base];
|
|
91
|
-
return [
|
|
92
|
-
base,
|
|
93
|
-
{ ...base, timeout: timeout * 2, allowedMcpServers: [] },
|
|
94
|
-
];
|
|
101
|
+
return [base, { ...base, timeout: timeout * 2, allowedMcpServers: [] }];
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
// ── Public: buildExecArgs ───────────────────────────────────────
|
|
98
105
|
|
|
99
106
|
export function buildExecArgs(opts = {}) {
|
|
100
|
-
const prompt = typeof opts.prompt ===
|
|
107
|
+
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
101
108
|
return buildGeminiCommand(prompt, opts.resultFile || null, {
|
|
102
109
|
model: opts.model,
|
|
103
110
|
allowedMcpServers: opts.mcpServers,
|
|
@@ -106,75 +113,50 @@ export function buildExecArgs(opts = {}) {
|
|
|
106
113
|
|
|
107
114
|
// ── Execution ───────────────────────────────────────────────────
|
|
108
115
|
|
|
109
|
-
async function runGemini(prompt, workdir, preflight, attempt) {
|
|
110
|
-
const dir = join(tmpdir(),
|
|
116
|
+
async function runGemini(prompt, workdir, preflight, attempt, lease) {
|
|
117
|
+
const dir = join(tmpdir(), "triflux-gemini-exec");
|
|
111
118
|
mkdirSync(dir, { recursive: true });
|
|
112
|
-
const resultFile = join(
|
|
119
|
+
const resultFile = join(
|
|
120
|
+
dir,
|
|
121
|
+
`gemini-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`,
|
|
122
|
+
);
|
|
113
123
|
const command = buildGeminiCommand(prompt, resultFile, {
|
|
114
124
|
model: attempt.model,
|
|
115
125
|
allowedMcpServers: attempt.allowedMcpServers,
|
|
116
126
|
excludeMcpServers: attempt.excludeMcpServers,
|
|
117
127
|
});
|
|
118
|
-
|
|
128
|
+
// PRD A1: lease.env 가 있으면 spawn env 에 적용 (예: GOOGLE_API_KEY 등 account-specific
|
|
129
|
+
// 환경변수). lease 가 null 이면 default 동작 유지 (부모 env inherit).
|
|
130
|
+
const spawnEnv =
|
|
131
|
+
lease?.env && typeof lease.env === "object"
|
|
132
|
+
? { ...process.env, ...lease.env }
|
|
133
|
+
: undefined;
|
|
134
|
+
return runProcess(command, workdir, attempt.timeout, {
|
|
135
|
+
resultFile,
|
|
136
|
+
inferStallMode,
|
|
137
|
+
spawnEnv,
|
|
138
|
+
});
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
// ── Public API ──────────────────────────────────────────────────
|
|
122
142
|
|
|
123
|
-
export function getCircuitState(
|
|
124
|
-
|
|
143
|
+
export async function getCircuitState() {
|
|
144
|
+
const brokerMod = await import("./account-broker.mjs");
|
|
145
|
+
if (!brokerMod.broker) return { state: "closed", failures: [] };
|
|
146
|
+
const snap = brokerMod.broker
|
|
147
|
+
.snapshot()
|
|
148
|
+
.filter((a) => a.provider === "gemini");
|
|
149
|
+
return snap.length
|
|
150
|
+
? { state: snap[0].circuitState, accounts: snap }
|
|
151
|
+
: { state: "closed", failures: [] };
|
|
125
152
|
}
|
|
126
153
|
|
|
127
|
-
export
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
};
|
|
154
|
+
export function execute(opts = {}) {
|
|
155
|
+
return executeWithCircuitBroker({
|
|
156
|
+
provider: "gemini",
|
|
157
|
+
runFn: runGemini,
|
|
158
|
+
preflightFn: (o) => runPreflight({ mcpServers: o.mcpServers }),
|
|
159
|
+
buildAttemptsFn: buildAttempts,
|
|
160
|
+
opts,
|
|
161
|
+
});
|
|
180
162
|
}
|
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({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
correlation_id
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|